From 2f4fd15e5290351c32d3f2754e29dea225e1d121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E8=96=84=E4=BA=91?= Date: Sun, 9 Aug 2026 22:13:18 +0800 Subject: [PATCH] chore: save current workspace progress --- .gitignore | 3 + .superpowers/brainstorm/.last-port | 1 + .superpowers/brainstorm/.last-token | 1 + .../content/dashboard-layout-options.html | 63 + .../content/speed-profile-strategies.html | 56 + .../waiting-full-segment-semantics.html | 6 + .../brainstorm/1212-1786007808/state/events | 2 + .../1212-1786007808/state/server-info | 1 + .../1212-1786007808/state/server-instance-id | 1 + .../1212-1786007808/state/server.pid | 1 + .../1515-1785381825/state/server-info | 1 + .../1515-1785381825/state/server-instance-id | 1 + .../1515-1785381825/state/server.pid | 1 + .../content/six-figure-layout.html | 37 + .../1528-1785381873/content/waiting.html | 3 + .../1528-1785381873/state/server-instance-id | 1 + .../1528-1785381873/state/server-stopped | 1 + .../1528-1785381873/state/server.pid | 1 + .../content/dashboard-layout.html | 73 + .../content/dashboard-scientific.html | 70 + .../content/dashboard-style.html | 74 + .../content/waiting-architecture.html | 6 + .../1612-1785939150/state/server-stopped | 1 + .../content/diagram-strategy.html | 54 + .../content/domain-planning-visual-v2.html | 89 + .../content/interaction-design-v3.html | 72 + .../content/interaction-layout.html | 69 + .../content/waiting-architecture.html | 6 + .../content/waiting-evidence-state.html | 6 + .../content/waiting-granularity.html | 6 + .../content/waiting-validation.html | 6 + .../1923-1785734653/state/server-stopped | 1 + .../content/dashboard-layout-options.html | 10 + .../content/dashboard-paper-thin-v2.html | 23 + .../content/waiting-targeted-repair.html | 6 + .../985-1786008552/state/server-instance-id | 1 + .../985-1786008552/state/server-stopped | 1 + .../985-1786008552/state/server.pid | 1 + .task8-sweep/AGV.cs | 534 + .task8-sweep/ChassisController.cs | 100 + .task8-sweep/ClumsyPilot.csproj | 53 + .task8-sweep/FleetCrabWalk.cs | 526 + .task8-sweep/FleetCurveWalk.cs | 411 + .task8-sweep/MovementTests.TireFollowing.cs | 606 + .task8-sweep/MovementTests.TwoLegDetect.cs | 136 + .task8-sweep/MovementTests.cs | 608 + .task8-sweep/Movements.cs | 326 + .../CoarsePath/Contracts/CoarsePathPoint.cs | 63 + .../Contracts/CoarsePathPointSource.cs | 14 + .../Contracts/GoalDirectionConstraint.cs | 14 + .../Contracts/HybridAStarConfiguration.cs | 83 + .../CoarsePath/Contracts/PathSegment.cs | 37 + .../Contracts/PlanningDiagnostics.cs | 65 + .../CoarsePath/Contracts/PlanningRequest.cs | 41 + .../CoarsePath/Contracts/PlanningResult.cs | 67 + .../CoarsePath/Contracts/PlanningStatus.cs | 56 + .../CoarsePath/Contracts/Pose2D.cs | 28 + .../CoarsePath/Contracts/TravelDirection.cs | 11 + .../CoarsePath/Contracts/VehicleParameters.cs | 28 + .../Facade/CoarsePathPlanningJob.cs | 45 + .../Facade/CoarsePathPlanningJobResult.cs | 45 + .../Facade/CoarsePathPlanningService.cs | 108 + .../CoarsePath/Facade/IPlanningDebugSink.cs | 29 + .../CoarsePath/Facade/PlanningDebugOptions.cs | 23 + .../CoarsePath/HybridAStarPlanner.cs | 264 + .../CoarsePath/Output/CoarsePathAssembler.cs | 216 + .../CoarsePath/Output/CoarsePathValidator.cs | 245 + .../CoarsePath/Output/PathBacktracker.cs | 167 + .../ParkrobTrajplanner/CoarsePath/README.md | 387 + .../CoarsePath/Search/BinaryMinHeap.cs | 146 + .../CoarsePath/Search/GoalToleranceChecker.cs | 61 + .../Search/GridDijkstraHeuristic.cs | 123 + .../CoarsePath/Search/HybridAStarNode.cs | 99 + .../CoarsePath/Search/HybridAStarNodeKey.cs | 67 + .../CoarsePath/Search/HybridAStarSearch.cs | 542 + .../CoarsePath/Search/MotionPrimitive.cs | 69 + .../Search/MotionPrimitiveGenerator.cs | 173 + .../CoarsePath/Search/SearchCostCalculator.cs | 98 + .../Test/CoarsePathScenarioFactory.cs | 507 + .../Test/MovementTest.CoarsePathTest.cs | 628 + .../Vehicle/FootprintCollisionChecker.cs | 160 + .../OrientedRectangleCellIntersection.cs | 33 + .../CoarsePath/Vehicle/VehicleFootprint.cs | 90 + .../CoarsePath/Vehicle/VehicleKinematics.cs | 40 + ...MR_HybridAStar_粗路径规划总体技术方案.html | 812 + .../AMR_HybridAStar_粗路径实施计划.md | 726 + .../AMR_轨迹规划技术路线_Agent版.md | 1194 + .../Map/Core/EnvironmentGridMap.cs | 92 + .../Map/Core/EnvironmentMapBuildResult.cs | 38 + .../Map/Core/EnvironmentMapBuilder.cs | 67 + .../Map/Core/MapBoundsMm.cs | 86 + .../Map/Core/MapBuildRequest.cs | 18 + .../Obstacles/AxisAlignedRectangleObstacle.cs | 31 + .../Map/Obstacles/CircleObstacle.cs | 29 + .../Map/Obstacles/IMapObstacle.cs | 12 + .../Map/Obstacles/MapObstacleRasterizer.cs | 98 + .../Planning/EuclideanDistanceTransform.cs | 123 + .../Map/Planning/ObstacleDistanceField.cs | 70 + .../Map/Planning/PlanningGridMap.cs | 88 + .../Map/Planning/PlanningMapAdapter.cs | 45 + .../Map/Planning/PlanningMapCache.cs | 65 + .../Map/PlanningMapBuildResult.cs | 66 + .../Map/PlanningMapFactory.cs | 184 + .../Map/PlanningMapRequest.cs | 35 + .task8-sweep/ParkrobTrajplanner/Map/README.md | 367 + .../Map/Sources/IMapObstacleSource.cs | 22 + .../Map/Sources/ManualObstacleSource.cs | 39 + .../Map/Sources/ObstacleProjectionResult.cs | 30 + .../Map/Sources/ObstacleSourceStatus.cs | 14 + .../Map/Sources/TwoLegObstacleProjector.cs | 32 + .../Map/Sources/TwoLegObstacleSource.cs | 24 + .../Map/Sources/TwoLegProjectionInput.cs | 51 + .../Map/Test/MovementTest.MapTest.cs | 133 + .../PlanningMapImageExportRequest.cs | 10 + .../PlanningMapImageExportResult.cs | 20 + .../Visualization/PlanningMapImageExporter.cs | 67 + .../Visualization/PlanningMapImageRenderer.cs | 37 + .../Test/Visualization/ValidatedPngWriter.cs | 98 + .../Algorithms/CubicBSplineSmoother.cs | 0 .../PathSmoothing/Algorithms/IPathSmoother.cs | 0 .../Algorithms/LocalCubicBezierSmoother.cs | 0 .../Algorithms/PiecewiseQuinticSmoother.cs | 0 .../Algorithms/SmoothingAlgorithmInput.cs | 0 .../Algorithms/SmoothingAlgorithmRunner.cs | 0 .../Algorithms/SmoothingCandidate.cs | 0 .../Algorithms/SmoothingOptionsSnapshot.cs | 0 .../PathSmoothingComparisonEntry.cs | 18 +- .../PathSmoothingComparisonRequest.cs | 0 .../PathSmoothingComparisonResult.cs | 0 .../Comparison/SmoothingMethodRanker.cs | 0 .../Comparison/SmoothingTimingSummary.cs | 0 .../Comparison/StableGeometryDigest.cs | 0 .../Contracts/CubicBSplineOptions.cs | 0 .../Contracts/LocalCubicBezierOptions.cs | 0 .../Contracts/LocalG2QuinticOptions.cs | 15 + .../Contracts/PathQualityMetrics.cs | 108 + .../Contracts/PathSmoothingConfiguration.cs | 53 + .../Contracts/PathSmoothingDiagnostics.cs | 80 + .../PathSmoothingRegionFailureReason.cs | 17 + .../Contracts/PathSmoothingRegionReport.cs | 83 + .../Contracts/PathSmoothingRegionStatus.cs | 8 + .../Contracts/PathSmoothingRequest.cs | 97 + .../Contracts/PathSmoothingResult.cs | 154 + .../Contracts/PathSmoothingStatus.cs | 16 + .../Contracts/PiecewiseQuinticOptions.cs | 0 .../Contracts/SmoothedPathPoint.cs | 100 + .../Contracts/SmoothedPathPointSource.cs | 11 + .../Contracts/SmoothedPathSegment.cs | 41 + .../Contracts/SmoothingMethod.cs | 10 + .../Facade/PathSmoothingComparisonService.cs | 231 + .../Facade/PathSmoothingService.cs | 305 + .../LocalG2/CurvatureTransition.cs | 55 + .../LocalG2/CurvatureTransitionDetector.cs | 260 + .../LocalG2/LocalG2CandidateBuilder.cs | 1242 + .../LocalG2/LocalG2CandidateEvaluator.cs | 559 + .../LocalG2/LocalG2CandidateGeometry.cs | 76 + .../LocalG2/LocalG2OptionsSnapshot.cs | 57 + .../LocalG2/LocalG2PathSplicer.cs | 134 + .../LocalG2/LocalG2PreSmoothingPipeline.cs | 234 + .../LocalG2/LocalG2RegionWorkOrder.cs | 107 + .../LocalG2/LocalG2SmoothingRegion.cs | 71 + .../LocalG2/LocalG2WindowPlanner.cs | 378 + .../LocalG2/QuinticHermiteCurve2D.cs | 124 + .../Processing/ArcLengthResampler.cs | 145 + .../Processing/PathGeometryAnalysis.cs | 72 + .../Processing/PathGeometryAnalyzer.cs | 490 + .../Processing/PathReferenceInterpolator.cs | 102 + .../Processing/PathSmoothingPreprocessor.cs | 201 + .../Processing/PreparedDirectionSegment.cs | 70 + .../PathSmoothing/Processing/PreparedPath.cs | 49 + .../Processing/RawPathBaselineBuilder.cs | 264 + .../Processing/SmoothingPoint2D.cs | 53 + .../PathSmoothing/README.md | 56 + .../Fixtures/path-smoothing-fixtures.json | 19451 ++++++++++++++++ .../Test/PathSmoothingComparisonDemo.cs | 161 + .../Test/SmoothingFixtureGenerator.cs | 260 + .../Test/SmoothingScenarioFactory.cs | 69 + .../Test/SmoothingScenarioFixture.cs | 156 + .../Test/SmoothingScenarioFixtureLoader.cs | 258 + .../Validation/SmoothedPathValidator.cs | 245 + .../Visualization/IeeeFigureStyle.cs | 26 + .../Visualization/SmoothingCsvWriter.cs | 43 + .../SmoothingFigureDefinition.cs | 109 + .../Visualization/SmoothingFigureKind.cs | 12 + .../Visualization/SmoothingFigureModel.cs | 185 + .../SmoothingFigureModelBuilder.cs | 170 + .../SmoothingFigureSetBuilder.cs | 171 + .../Visualization/SmoothingFontResolver.cs | 140 + .../Visualization/SmoothingPngRenderer.cs | 287 + .../SmoothingReportExportRequest.cs | 11 + .../SmoothingReportExportResult.cs | 49 + .../Visualization/SmoothingReportExporter.cs | 130 + .../Visualization/SmoothingSvgRenderer.cs | 189 + .../ParkrobTrajplanner/Utils/AngleMath.cs | 34 + .../Utils/CoordinateTransform.cs | 18 + .../ParkrobTrajplanner/Utils/GridIndex.cs | 15 + .../ParkrobTrajplanner/Utils/NumericGuard.cs | 14 + .../Utils/PlanningOperationBudget.cs | 81 + .../ParkrobTrajplanner/Utils/UnitConverter.cs | 11 + .task8-sweep/PilotConfig.cs | 334 + .task8-sweep/PilotDefinition.cs | 2128 ++ .task8-sweep/PlaygroundWebApi.cs | 81 + .task8-sweep/TireFollowing.cs | 511 + .task8-sweep/VehicleSyncBinaryCodec.cs | 277 + .task8-sweep/VehicleSyncModels.cs | 75 + .task8-sweep/ref/CommonUsage.dll | Bin 0 -> 134144 bytes .task8-sweep/ref/LessokajiWeaverUtilities.dll | Bin 0 -> 10240 bytes .task8-sweep/ref/MDCSToolBox.dll | Bin 0 -> 3580416 bytes .task8-sweep/ref/RefClumsyCore.dll | Bin 0 -> 95744 bytes .task8-sweep/ref/RefClumsyDance.dll | Bin 0 -> 55296 bytes .task8-sweep/ref/RefFundamentalLib.dll | Bin 0 -> 36352 bytes .../PathSmoothingPngVerificationHost.csproj | 13 + .../Program.cs | 360 + .../generate_path_smoothing_fixtures.ps1 | 15 + .../tests/run_path_smoothing_comparison.ps1 | 32 + .../tests/verify_coarse_path_collision.ps1 | 130 + .../tests/verify_coarse_path_integration.ps1 | 460 + .../tests/verify_coarse_path_search.ps1 | 411 + .task8-sweep/tests/verify_coarse_path_ui.ps1 | 122 + .../verify_path_smoothing_algorithm_input.ps1 | 0 .../tests/verify_path_smoothing_bezier.ps1 | 0 .../tests/verify_path_smoothing_bspline.ps1 | 0 .../verify_path_smoothing_comparison.ps1 | 0 .../tests/verify_path_smoothing_contracts.ps1 | 4 +- .../verify_path_smoothing_documentation.ps1 | 65 + .../tests/verify_path_smoothing_fixtures.ps1 | 119 + .../tests/verify_path_smoothing_geometry.ps1 | 642 + .../verify_path_smoothing_integration.ps1 | 0 ...ify_path_smoothing_local_g2_candidates.ps1 | 252 + .../verify_path_smoothing_local_g2_curve.ps1 | 118 + ...rify_path_smoothing_local_g2_detection.ps1 | 129 + ...fy_path_smoothing_local_g2_integration.ps1 | 136 + .../tests/verify_path_smoothing_png.ps1 | 18 + .../tests/verify_path_smoothing_quintic.ps1 | 0 .../tests/verify_path_smoothing_runner.ps1 | 0 .../tests/verify_path_smoothing_service.ps1 | 7 + .../tests/verify_path_smoothing_svg_csv.ps1 | 188 + .../verify_path_smoothing_validation.ps1 | 175 + .../tests/verify_planning_map_adapter.ps1 | 47 + .../verify_planning_map_documentation.ps1 | 58 + .../tests/verify_planning_map_factory.ps1 | 88 + .../tests/verify_planning_map_image.ps1 | 31 + .../tests/verify_planning_map_test_config.ps1 | 23 + ClumsyPilot/ClumsyPilot.csproj | 19 +- .../CoarsePath/Contracts/CoarsePathPoint.cs | 63 + .../Contracts/CoarsePathPointSource.cs | 14 + .../Contracts/GoalDirectionConstraint.cs | 14 + .../Contracts/HybridAStarConfiguration.cs | 83 + .../CoarsePath/Contracts/PathSegment.cs | 37 + .../Contracts/PlanningDiagnostics.cs | 65 + .../CoarsePath/Contracts/PlanningRequest.cs | 41 + .../CoarsePath/Contracts/PlanningResult.cs | 67 + .../CoarsePath/Contracts/PlanningStatus.cs | 56 + .../CoarsePath/Contracts/Pose2D.cs | 28 + .../CoarsePath/Contracts/TravelDirection.cs | 11 + .../CoarsePath/Contracts/VehicleParameters.cs | 28 + .../Facade/CoarsePathPlanningJob.cs | 45 + .../Facade/CoarsePathPlanningJobResult.cs | 45 + .../Facade/CoarsePathPlanningService.cs | 108 + .../CoarsePath/Facade/IPlanningDebugSink.cs | 29 + .../CoarsePath/Facade/PlanningDebugOptions.cs | 23 + .../CoarsePath/HybridAStarPlanner.cs | 264 + .../CoarsePath/Output/CoarsePathValidator.cs | 245 + .../CoarsePath/Output/PathBacktracker.cs | 167 + .../ParkrobTrajplanner/CoarsePath/README.md | 387 + .../CoarsePath/Search/BinaryMinHeap.cs | 146 + .../CoarsePath/Search/GoalToleranceChecker.cs | 61 + .../Search/GridDijkstraHeuristic.cs | 123 + .../CoarsePath/Search/HybridAStarNode.cs | 99 + .../CoarsePath/Search/HybridAStarNodeKey.cs | 67 + .../CoarsePath/Search/HybridAStarSearch.cs | 542 + .../CoarsePath/Search/MotionPrimitive.cs | 69 + .../Search/MotionPrimitiveGenerator.cs | 173 + .../CoarsePath/Search/SearchCostCalculator.cs | 98 + .../Test/CoarsePathScenarioFactory.cs | 507 + .../Test/MovementTest.CoarsePathTest.cs | 628 + .../Vehicle/FootprintCollisionChecker.cs | 160 + .../OrientedRectangleCellIntersection.cs | 33 + .../CoarsePath/Vehicle/VehicleFootprint.cs | 90 + .../CoarsePath/Vehicle/VehicleKinematics.cs | 40 + ...MR_HybridAStar_粗路径规划总体技术方案.html | 812 + .../Configuration/EmPlannerConfiguration.cs | 6 +- .../EMPlanner/Facade/EmPlanningService.cs | 36 +- .../Lateral/LateralConstraintBuilder.cs | 20 +- .../Lateral/LateralCurvatureLinearization.cs | 101 + .../Lateral/LateralObjectiveBuilder.cs | 108 +- .../Lateral/SequentialConvexOptimizer.cs | 37 +- .../SequentialLongitudinalOptimizer.cs | 10 +- .../Trajectory/EmTrajectoryAssembler.cs | 2 +- .../Trajectory/LateralPathInterpolator.cs | 8 +- .../Validation/EmTrajectoryValidator.cs | 30 +- .../AMR_HybridAStar_粗路径实施计划.md | 1009 +- .../AMR_轨迹规划技术路线_Agent版.md | 1194 + .../Map/Core/EnvironmentGridMap.cs | 92 + .../Map/Core/EnvironmentMapBuildResult.cs | 38 + .../Map/Core/EnvironmentMapBuilder.cs | 67 + .../Map/Core/MapBoundsMm.cs | 86 + .../Map/Core/MapBuildRequest.cs | 18 + .../Obstacles/AxisAlignedRectangleObstacle.cs | 31 + .../Map/Obstacles/CircleObstacle.cs | 29 + .../Map/Obstacles/IMapObstacle.cs | 12 + .../Map/Obstacles/MapObstacleRasterizer.cs | 98 + .../Planning/EuclideanDistanceTransform.cs | 123 + .../Map/Planning/ObstacleDistanceField.cs | 70 + .../Map/Planning/PlanningGridMap.cs | 88 + .../Map/Planning/PlanningMapAdapter.cs | 45 + .../Map/Planning/PlanningMapCache.cs | 65 + .../Map/PlanningMapBuildResult.cs | 66 + .../Map/PlanningMapFactory.cs | 184 + .../Map/PlanningMapRequest.cs | 35 + ClumsyPilot/ParkrobTrajplanner/Map/README.md | 367 + .../Map/Sources/IMapObstacleSource.cs | 22 + .../Map/Sources/ManualObstacleSource.cs | 39 + .../Map/Sources/ObstacleProjectionResult.cs | 30 + .../Map/Sources/ObstacleSourceStatus.cs | 14 + .../Map/Sources/TwoLegObstacleProjector.cs | 32 + .../Map/Sources/TwoLegObstacleSource.cs | 24 + .../Map/Sources/TwoLegProjectionInput.cs | 51 + .../Map/Test/MovementTest.MapTest.cs | 133 + .../PlanningMapImageExportRequest.cs | 10 + .../PlanningMapImageExportResult.cs | 20 + .../Visualization/PlanningMapImageExporter.cs | 67 + .../Visualization/PlanningMapImageRenderer.cs | 37 + .../Test/Visualization/ValidatedPngWriter.cs | 98 + .../Contracts/PathSmoothingConfiguration.cs | 46 +- .../Contracts/PathSmoothingDiagnostics.cs | 64 +- .../Contracts/PathSmoothingRequest.cs | 30 +- .../Contracts/PathSmoothingResult.cs | 84 +- .../Contracts/PathSmoothingStatus.cs | 4 +- .../Contracts/SmoothedPathPointSource.cs | 9 +- .../Contracts/SmoothingMethod.cs | 5 +- .../Facade/PathSmoothingComparisonService.cs | 83 +- .../Facade/PathSmoothingService.cs | 211 +- .../LocalG2/LocalG2CandidateEvaluator.cs | 27 +- .../LocalG2/LocalG2PreSmoothingPipeline.cs | 242 + .../PathSmoothingComparisonEntry.cs | 117 + .../PathSmoothingComparisonRequest.cs | 33 + .../PathSmoothingComparisonResult.cs | 45 + .../Comparison/SmoothingTimingSummary.cs | 90 + .../Output/Comparison/StableGeometryDigest.cs | 111 + .../Output/Visualization/IeeeFigureStyle.cs | 12 + .../Visualization/SmoothingCsvWriter.cs | 42 + .../SmoothingFigureDefinition.cs | 111 + .../Visualization/SmoothingFigureKind.cs | 11 + .../Visualization/SmoothingFigureModel.cs | 207 + .../SmoothingFigureModelBuilder.cs | 180 + .../SmoothingFigureSetBuilder.cs | 265 + .../Visualization/SmoothingFontResolver.cs | 140 + .../Visualization/SmoothingPngRenderer.cs | 287 + .../SmoothingReportExportRequest.cs | 11 + .../SmoothingReportExportResult.cs | 49 + .../Visualization/SmoothingReportExporter.cs | 130 + .../Visualization/SmoothingSvgRenderer.cs | 189 + .../Processing/RawPathBaselineBuilder.cs | 4 +- .../PathSmoothing/README.md | 137 + .../Fixtures/path-smoothing-fixtures.json | 16 +- .../LocalG2DiagnosticVisualizationDemo.cs | 171 + .../Test/PathSmoothingComparisonDemo.cs | 164 + .../Test/SmoothingScenarioFactory.cs | 2 +- .../Test/SmoothingScenarioFixtureLoader.cs | 18 +- .../Validation/CurvatureLimitPolicy.cs | 29 + .../Validation/SmoothedPathValidator.cs | 48 +- .../ParkrobTrajplanner/Utils/AngleMath.cs | 34 + .../Utils/CoordinateTransform.cs | 18 + .../ParkrobTrajplanner/Utils/GridIndex.cs | 15 + .../ParkrobTrajplanner/Utils/NumericGuard.cs | 14 + .../Utils/PlanningOperationBudget.cs | 81 + .../ParkrobTrajplanner/Utils/UnitConverter.cs | 11 + .../MovementTest.TrajectoryObservationTest.cs | 16 +- .../TrajectoryObservationPipeline.cs | 131 +- .../TrajectoryObservationSegmentTracker.cs | 10 +- .../EmPlanningServiceChecks.cs | 113 +- .../EMPlannerVerificationHost/FakeQpSolver.cs | 26 + .../LateralIntegrationChecks.cs | 52 +- .../LateralModelChecks.cs | 39 + .../LongitudinalIntegrationChecks.cs | 12 + .../TrajectoryChecks.cs | 25 + .../TrajectoryObservationChecks.cs | 133 +- .../TrajectoryObservationSegmentChecks.cs | 29 + .../PathSmoothingPngVerificationHost.csproj | 13 + .../Program.cs | 590 + .../run_local_g2_diagnostic_visualization.ps1 | 28 + .../tests/run_path_smoothing_comparison.ps1 | 32 + .../tests/verify_coarse_path_collision.ps1 | 130 + .../tests/verify_coarse_path_search.ps1 | 411 + ClumsyPilot/tests/verify_coarse_path_ui.ps1 | 122 + .../verify_path_smoothing_documentation.ps1 | 68 + .../tests/verify_path_smoothing_fixtures.ps1 | 2 +- ...ify_path_smoothing_local_g2_candidates.ps1 | 44 +- ...smoothing_local_g2_diagnostic_contract.ps1 | 20 + ...hing_local_g2_diagnostic_visualization.ps1 | 11 + ...fy_path_smoothing_local_g2_integration.ps1 | 98 + ...fy_path_smoothing_local_g2_only_layout.ps1 | 75 + .../tests/verify_path_smoothing_png.ps1 | 30 + .../tests/verify_planning_map_adapter.ps1 | 47 + .../verify_planning_map_documentation.ps1 | 58 + .../tests/verify_planning_map_factory.ps1 | 88 + .../tests/verify_planning_map_image.ps1 | 31 + .../tests/verify_planning_map_test_config.ps1 | 23 + .../checkpoints/120019-daily-summary-job.json | 22 + ...mary-job-skill-progress-daily-summary.json | 238 + ...tecture-ls-optimization-daily-summary.json | 112 + dailywork_report/Map_rep/.gitkeep | 0 dailywork_report/coarsepath_rep/.gitkeep | 0 ...job-skill-progress-daily-summary-report.md | 92 + ...-progress-daily-summary-visualization.html | 397 + ...re-ls-optimization-daily-summary-report.md | 55 + ...imization-daily-summary-visualization.html | 419 + ...cal-g2-quintic-hermite-algorithm-report.md | 183 + ...local-g2-issues-and-next-actions-report.md | 185 + ...-curvature-excursion-feasibility-report.md | 128 + ...e-excursion-feasibility-visualization.html | 616 + .../2026-07-22-trap-map-image-and-console.md | 266 + .../plans/2026-07-23-trap-map-pure-png.md | 125 + ...26-07-27-hybrid-astar-p0-implementation.md | 427 + .../plans/2026-07-27-map-documentation.md | 242 + ...026-07-27-p1-coarse-path-ui-integration.md | 534 + .../2026-07-27-planning-operation-budget.md | 369 + ...26-07-28-coarse-path-live-amr-scenarios.md | 436 + .../2026-07-28-coarse-path-search-elapsed.md | 256 + ...2026-07-28-coarse-path-test-diagnostics.md | 837 + ...026-07-28-coarsepath-readme-restructure.md | 185 + .../2026-07-28-p1-manual-obstacle-input.md | 365 + ...-07-30-path-smoothing-six-figure-report.md | 73 + .../2026-07-31-local-g2-dailywork-reports.md | 249 + ...l-g2-interactive-visualization-redesign.md | 404 + ...08-02-local-g2-diagnostic-visualization.md | 558 + ...ummary-job-domain-visualization-upgrade.md | 915 + .../2026-08-03-daily-summary-job-skill.md | 432 + ...l-g2-only-path-smoothing-reorganization.md | 106 + .../2026-08-04-em-observation-movementtest.md | 457 + .../2026-08-05-em-observation-diagnostics.md | 334 + ...-09-em-full-direction-correctness-fixes.md | 227 + ...07-22-trap-map-image-and-console-design.md | 111 + .../2026-07-23-trap-map-pure-png-design.md | 52 + ...6-07-26-hybrid-astar-coarse-path-design.md | 481 +- ...7-hybrid-astar-p0-implementation-design.md | 94 + .../2026-07-27-map-documentation-design.md | 56 + ...27-p1-coarse-path-ui-integration-design.md | 114 + ...-07-27-planning-operation-budget-design.md | 59 + ...8-coarse-path-live-amr-scenarios-design.md | 4 +- ...07-28-coarse-path-search-elapsed-design.md | 66 + ...28-coarsepath-readme-restructure-design.md | 46 + ...6-07-28-p1-manual-obstacle-input-design.md | 71 + ...path-smoothing-six-figure-report-design.md | 38 + ...07-31-local-g2-dailywork-reports-design.md | 62 + ...job-domain-visualization-upgrade-design.md | 295 + ...26-08-03-daily-summary-job-skill-design.md | 188 + emplanner-daily-report.json | 99 + 449 files changed, 76593 insertions(+), 971 deletions(-) create mode 100644 .superpowers/brainstorm/.last-port create mode 100644 .superpowers/brainstorm/.last-token create mode 100644 .superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html create mode 100644 .superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html create mode 100644 .superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html create mode 100644 .superpowers/brainstorm/1212-1786007808/state/events create mode 100644 .superpowers/brainstorm/1212-1786007808/state/server-info create mode 100644 .superpowers/brainstorm/1212-1786007808/state/server-instance-id create mode 100644 .superpowers/brainstorm/1212-1786007808/state/server.pid create mode 100644 .superpowers/brainstorm/1515-1785381825/state/server-info create mode 100644 .superpowers/brainstorm/1515-1785381825/state/server-instance-id create mode 100644 .superpowers/brainstorm/1515-1785381825/state/server.pid create mode 100644 .superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html create mode 100644 .superpowers/brainstorm/1528-1785381873/content/waiting.html create mode 100644 .superpowers/brainstorm/1528-1785381873/state/server-instance-id create mode 100644 .superpowers/brainstorm/1528-1785381873/state/server-stopped create mode 100644 .superpowers/brainstorm/1528-1785381873/state/server.pid create mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html create mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html create mode 100644 .superpowers/brainstorm/1612-1785939150/content/dashboard-style.html create mode 100644 .superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html create mode 100644 .superpowers/brainstorm/1612-1785939150/state/server-stopped create mode 100644 .superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/interaction-layout.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html create mode 100644 .superpowers/brainstorm/1923-1785734653/content/waiting-validation.html create mode 100644 .superpowers/brainstorm/1923-1785734653/state/server-stopped create mode 100644 .superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html create mode 100644 .superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html create mode 100644 .superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html create mode 100644 .superpowers/brainstorm/985-1786008552/state/server-instance-id create mode 100644 .superpowers/brainstorm/985-1786008552/state/server-stopped create mode 100644 .superpowers/brainstorm/985-1786008552/state/server.pid create mode 100644 .task8-sweep/AGV.cs create mode 100644 .task8-sweep/ChassisController.cs create mode 100644 .task8-sweep/ClumsyPilot.csproj create mode 100644 .task8-sweep/FleetCrabWalk.cs create mode 100644 .task8-sweep/FleetCurveWalk.cs create mode 100644 .task8-sweep/MovementTests.TireFollowing.cs create mode 100644 .task8-sweep/MovementTests.TwoLegDetect.cs create mode 100644 .task8-sweep/MovementTests.cs create mode 100644 .task8-sweep/Movements.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/README.md create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html create mode 100644 .task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md create mode 100644 .task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/README.md create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs (85%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs (100%) rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs (100%) create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs rename {ClumsyPilot => .task8-sweep}/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs (100%) create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs create mode 100644 .task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs create mode 100644 .task8-sweep/PilotConfig.cs create mode 100644 .task8-sweep/PilotDefinition.cs create mode 100644 .task8-sweep/PlaygroundWebApi.cs create mode 100644 .task8-sweep/TireFollowing.cs create mode 100644 .task8-sweep/VehicleSyncBinaryCodec.cs create mode 100644 .task8-sweep/VehicleSyncModels.cs create mode 100644 .task8-sweep/ref/CommonUsage.dll create mode 100644 .task8-sweep/ref/LessokajiWeaverUtilities.dll create mode 100644 .task8-sweep/ref/MDCSToolBox.dll create mode 100644 .task8-sweep/ref/RefClumsyCore.dll create mode 100644 .task8-sweep/ref/RefClumsyDance.dll create mode 100644 .task8-sweep/ref/RefFundamentalLib.dll create mode 100644 .task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj create mode 100644 .task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs create mode 100644 .task8-sweep/tests/generate_path_smoothing_fixtures.ps1 create mode 100644 .task8-sweep/tests/run_path_smoothing_comparison.ps1 create mode 100644 .task8-sweep/tests/verify_coarse_path_collision.ps1 create mode 100644 .task8-sweep/tests/verify_coarse_path_integration.ps1 create mode 100644 .task8-sweep/tests/verify_coarse_path_search.ps1 create mode 100644 .task8-sweep/tests/verify_coarse_path_ui.ps1 rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_algorithm_input.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_bezier.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_bspline.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_comparison.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_contracts.ps1 (99%) create mode 100644 .task8-sweep/tests/verify_path_smoothing_documentation.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_fixtures.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_geometry.ps1 rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_integration.ps1 (100%) create mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_png.ps1 rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_quintic.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_runner.ps1 (100%) rename {ClumsyPilot => .task8-sweep}/tests/verify_path_smoothing_service.ps1 (96%) create mode 100644 .task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 create mode 100644 .task8-sweep/tests/verify_path_smoothing_validation.ps1 create mode 100644 .task8-sweep/tests/verify_planning_map_adapter.ps1 create mode 100644 .task8-sweep/tests/verify_planning_map_documentation.ps1 create mode 100644 .task8-sweep/tests/verify_planning_map_factory.ps1 create mode 100644 .task8-sweep/tests/verify_planning_map_image.ps1 create mode 100644 .task8-sweep/tests/verify_planning_map_test_config.ps1 create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html create mode 100644 ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/README.md create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonEntry.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingTimingSummary.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/StableGeometryDigest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/IeeeFigureStyle.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingCsvWriter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureDefinition.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureKind.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModel.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModelBuilder.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureSetBuilder.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFontResolver.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingPngRenderer.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExporter.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingSvgRenderer.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs create mode 100644 ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj create mode 100644 ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs create mode 100644 ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 create mode 100644 ClumsyPilot/tests/run_path_smoothing_comparison.ps1 create mode 100644 ClumsyPilot/tests/verify_coarse_path_collision.ps1 create mode 100644 ClumsyPilot/tests/verify_coarse_path_search.ps1 create mode 100644 ClumsyPilot/tests/verify_coarse_path_ui.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 create mode 100644 ClumsyPilot/tests/verify_path_smoothing_png.ps1 create mode 100644 ClumsyPilot/tests/verify_planning_map_adapter.ps1 create mode 100644 ClumsyPilot/tests/verify_planning_map_documentation.ps1 create mode 100644 ClumsyPilot/tests/verify_planning_map_factory.ps1 create mode 100644 ClumsyPilot/tests/verify_planning_map_image.ps1 create mode 100644 ClumsyPilot/tests/verify_planning_map_test_config.ps1 create mode 100644 dailywork_report/.daily-summary-job/2026-08-03/checkpoints/120019-daily-summary-job.json create mode 100644 dailywork_report/.daily-summary-job/2026-08-03/summaries/daily-summary-job_rep/01-daily-summary-job-skill-progress-daily-summary.json create mode 100644 dailywork_report/.daily-summary-job/2026-08-04/summaries/emplanner_rep/01-emplanner-architecture-ls-optimization-daily-summary.json create mode 100644 dailywork_report/Map_rep/.gitkeep create mode 100644 dailywork_report/coarsepath_rep/.gitkeep create mode 100644 dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-report.md create mode 100644 dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-visualization.html create mode 100644 dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-report.md create mode 100644 dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-visualization.html create mode 100644 dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md create mode 100644 dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md create mode 100644 dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-report.md create mode 100644 dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-visualization.html create mode 100644 docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md create mode 100644 docs/superpowers/plans/2026-07-23-trap-map-pure-png.md create mode 100644 docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md create mode 100644 docs/superpowers/plans/2026-07-27-map-documentation.md create mode 100644 docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md create mode 100644 docs/superpowers/plans/2026-07-27-planning-operation-budget.md create mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md create mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md create mode 100644 docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md create mode 100644 docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md create mode 100644 docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md create mode 100644 docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md create mode 100644 docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md create mode 100644 docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md create mode 100644 docs/superpowers/plans/2026-08-02-local-g2-diagnostic-visualization.md create mode 100644 docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md create mode 100644 docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md create mode 100644 docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md create mode 100644 docs/superpowers/plans/2026-08-04-em-observation-movementtest.md create mode 100644 docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md create mode 100644 docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md create mode 100644 docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md create mode 100644 docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md create mode 100644 docs/superpowers/specs/2026-07-27-hybrid-astar-p0-implementation-design.md create mode 100644 docs/superpowers/specs/2026-07-27-map-documentation-design.md create mode 100644 docs/superpowers/specs/2026-07-27-p1-coarse-path-ui-integration-design.md create mode 100644 docs/superpowers/specs/2026-07-27-planning-operation-budget-design.md create mode 100644 docs/superpowers/specs/2026-07-28-coarse-path-search-elapsed-design.md create mode 100644 docs/superpowers/specs/2026-07-28-coarsepath-readme-restructure-design.md create mode 100644 docs/superpowers/specs/2026-07-28-p1-manual-obstacle-input-design.md create mode 100644 docs/superpowers/specs/2026-07-30-path-smoothing-six-figure-report-design.md create mode 100644 docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md create mode 100644 docs/superpowers/specs/2026-08-03-daily-summary-job-domain-visualization-upgrade-design.md create mode 100644 docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md create mode 100644 emplanner-daily-report.json diff --git a/.gitignore b/.gitignore index bbeebc2..a3d7c53 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ appsettings.Development.json # OS files Thumbs.db .DS_Store + +#Unfinished Traplanner +TrajPlanner \ No newline at end of file diff --git a/.superpowers/brainstorm/.last-port b/.superpowers/brainstorm/.last-port new file mode 100644 index 0000000..d4ad060 --- /dev/null +++ b/.superpowers/brainstorm/.last-port @@ -0,0 +1 @@ +52380 \ No newline at end of file diff --git a/.superpowers/brainstorm/.last-token b/.superpowers/brainstorm/.last-token new file mode 100644 index 0000000..2b306ca --- /dev/null +++ b/.superpowers/brainstorm/.last-token @@ -0,0 +1 @@ +c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248 \ No newline at end of file diff --git a/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html b/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html new file mode 100644 index 0000000..069c0be --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/content/dashboard-layout-options.html @@ -0,0 +1,63 @@ + + +

网页主界面采用哪种观察布局?

+

所有方案都修复页签串页、增加 X/Y 数值刻度与单位、图例、图层显隐、框选缩放、滚轮缩放、复位和全屏查看。

+ +
+
+

A · 地图主视图

路径图最大,状态在右侧;图表作为底部工作区。适合先看全局几何。

+
+
FullDirectionSegment · OBSERVE_ONLY图层 ▣ 重置视图
+
+ − ⛶ 图层
+
终点误差 0.8 cm / 1.2°
v=0 · a=0
制动起点 S=6.4 m
+
框选放大 复位 全屏ST:t (s) / PathS (m)
+
路径总览 LS/ST 运动学 诊断与配置
+
+
+ +
+
推荐

B · 地图与分析并列

左侧完整路径,右侧同时看当前选中的 ST/速度图与关键状态,减少来回切页。

+
+
方向段 0 · Forward · 完整规划成功导出快照 图层
+
+ − ⛶
+
ST:完整起步—巡航—停车
巡航目标 1.0 m/s
终点 3 cm / 5° 内
无零进度异常
+
路径总览 LS/ST 曲率 速度/加速度/jerk 周期诊断
+
+
+ +
+

C · 多图分析墙

地图在上方,LS/ST/速度三图并排;适合桌面大屏同时比较数据。

+
+
完整方向段分析同步缩放 复位全部
+
+ − ⛶ 图层
+
LS
ST
速度
+
基础图组 运动学图组 终点验证 配置
+
+
+
diff --git a/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html b/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html new file mode 100644 index 0000000..0ed43dc --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/content/speed-profile-strategies.html @@ -0,0 +1,56 @@ + + +

每个方向段应如何规划“起步—巡航—停车”?

+

三种实现方式都保持 OBSERVE_ONLY。点击选择;推荐 C,它既表达完整方向段,又保留实时修正能力。

+ +
+
+

A · 固定距离开始减速

+ + + + 起步巡航固定点制动 + +

实现简单,但速度、载荷或当前加速度变化后,固定制动点可能过早或来不及停车。

+
+ +
+

B · 整段一次性联合优化

+ + + + 整段时间与速度共同求解 + +

完整性最好,但长方向段会产生很大的优化问题;现场参数变化时,整段重新求解可能很慢。

+
+ +
+ 推荐 +

C · 全局速度包络 + 滚动精化

+ + + + jerk 起步受限巡航动态停车包络 + +

先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。

+
+
+ +
+开始减速条件:剩余距离 ≤ jerk 受限停车距离(当前 v, 当前 a, 最大减速度, 最大 jerk) + 重规划安全余量 +
diff --git a/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html b/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html new file mode 100644 index 0000000..dbf016f --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/content/waiting-full-segment-semantics.html @@ -0,0 +1,6 @@ +
+
+

正在确认完整方向段规划语义

+

主产物:一次优化得到完整当前方向段的 LS 与 ST;网页显示完整轨迹,而不是短窗口片段。

+
+
diff --git a/.superpowers/brainstorm/1212-1786007808/state/events b/.superpowers/brainstorm/1212-1786007808/state/events new file mode 100644 index 0000000..73621dd --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/state/events @@ -0,0 +1,2 @@ +{"type":"click","text":"推荐\n C · 全局速度包络 + 滚动精化\n \n \n \n jerk 起步受限巡航动态停车包络\n \n 先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。","choice":"c-hybrid","id":null,"timestamp":1786007937984} +{"type":"click","text":"推荐\n C · 全局速度包络 + 滚动精化\n \n \n \n jerk 起步受限巡航动态停车包络\n \n 先为完整方向段计算速度上限与停车包络,再按短周期重算当前轨迹;能主动起步,也能随实时状态调整制动点。","choice":"c-hybrid","id":null,"timestamp":1786007938998} diff --git a/.superpowers/brainstorm/1212-1786007808/state/server-info b/.superpowers/brainstorm/1212-1786007808/state/server-info new file mode 100644 index 0000000..3735700 --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/state/server-info @@ -0,0 +1 @@ +{"type":"server-started","port":52380,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:52380/?key=c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248","screen_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1212-1786007808\\content","state_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1212-1786007808\\state","idle_timeout_ms":14400000} diff --git a/.superpowers/brainstorm/1212-1786007808/state/server-instance-id b/.superpowers/brainstorm/1212-1786007808/state/server-instance-id new file mode 100644 index 0000000..31120e7 --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/state/server-instance-id @@ -0,0 +1 @@ +823e5463adedce4ebfd4c6a032a5fb950de021e730acb9aa diff --git a/.superpowers/brainstorm/1212-1786007808/state/server.pid b/.superpowers/brainstorm/1212-1786007808/state/server.pid new file mode 100644 index 0000000..d6e9296 --- /dev/null +++ b/.superpowers/brainstorm/1212-1786007808/state/server.pid @@ -0,0 +1 @@ +1224 diff --git a/.superpowers/brainstorm/1515-1785381825/state/server-info b/.superpowers/brainstorm/1515-1785381825/state/server-info new file mode 100644 index 0000000..363a337 --- /dev/null +++ b/.superpowers/brainstorm/1515-1785381825/state/server-info @@ -0,0 +1 @@ +{"type":"server-started","port":52380,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:52380/?key=c619697a72212b481e509ebd1c9023d987d76f98e8d335c1a428bd2f2e641248","screen_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1515-1785381825\\content","state_dir":"D:\\Users\\Desktop\\项目\\prakrobot\\ParkingRobot\\.superpowers\\brainstorm\\1515-1785381825\\state","idle_timeout_ms":14400000} diff --git a/.superpowers/brainstorm/1515-1785381825/state/server-instance-id b/.superpowers/brainstorm/1515-1785381825/state/server-instance-id new file mode 100644 index 0000000..8b7b542 --- /dev/null +++ b/.superpowers/brainstorm/1515-1785381825/state/server-instance-id @@ -0,0 +1 @@ +ee4120676fb587be5e6f7996276b30b3e88f37f9781d30a6 diff --git a/.superpowers/brainstorm/1515-1785381825/state/server.pid b/.superpowers/brainstorm/1515-1785381825/state/server.pid new file mode 100644 index 0000000..c16dfe7 --- /dev/null +++ b/.superpowers/brainstorm/1515-1785381825/state/server.pid @@ -0,0 +1 @@ +1527 diff --git a/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html b/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html new file mode 100644 index 0000000..315b29d --- /dev/null +++ b/.superpowers/brainstorm/1528-1785381873/content/six-figure-layout.html @@ -0,0 +1,37 @@ +

六张点图报告:布局草图

+

所有轨迹均以完整采样点绘制,不使用任何连线。每张俯瞰图按路径包围盒加小边距取景,并保持 X/Y 等比例。

+ +
+
1. 原始粗路径 · 俯瞰
+ + + X (m)Y (m) +

保留障碍物、起终点与灰色粗路径点。

+ +
2. 四方法 · 点集对比
+ + + + + + ● 粗路径● B样条● Bézier● 五次 +

仅四组路径点、坐标轴和图例;无障碍物、无起终点。

+ +
3. B 样条 · 俯瞰
+ X (m)Y (m) +

灰色粗路径点作参照;蓝色为全部 B 样条输出点。

+ +
4. Bézier · 俯瞰
+ X (m)Y (m) +

灰色粗路径点作参照;橙色为全部 Bézier 输出点。

+ +
5. 五次 · 俯瞰
+ X (m)Y (m) +

灰色粗路径点作参照;绿色为全部五次输出点。

+ +
6. 曲率 · 全方法点图
+ s (m)κ (m⁻¹)● 粗路径● B样条● Bézier● 五次 +

四组曲率采样点、统一坐标轴和图例;不绘制曲线。

+
+ +

请确认:这正是将被导出的六张独立 PNG + SVG 图的内容划分。看完后直接回复“确认”或指出需要调整的那一张。

diff --git a/.superpowers/brainstorm/1528-1785381873/content/waiting.html b/.superpowers/brainstorm/1528-1785381873/content/waiting.html new file mode 100644 index 0000000..a574018 --- /dev/null +++ b/.superpowers/brainstorm/1528-1785381873/content/waiting.html @@ -0,0 +1,3 @@ +
+

布局已确认,继续在终端中准备实现。

+
diff --git a/.superpowers/brainstorm/1528-1785381873/state/server-instance-id b/.superpowers/brainstorm/1528-1785381873/state/server-instance-id new file mode 100644 index 0000000..8527a1f --- /dev/null +++ b/.superpowers/brainstorm/1528-1785381873/state/server-instance-id @@ -0,0 +1 @@ +fe7e537ea1c8795202b14034148de7c503a50e4319e76d42 diff --git a/.superpowers/brainstorm/1528-1785381873/state/server-stopped b/.superpowers/brainstorm/1528-1785381873/state/server-stopped new file mode 100644 index 0000000..8af0225 --- /dev/null +++ b/.superpowers/brainstorm/1528-1785381873/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1785396457111} diff --git a/.superpowers/brainstorm/1528-1785381873/state/server.pid b/.superpowers/brainstorm/1528-1785381873/state/server.pid new file mode 100644 index 0000000..bf9b507 --- /dev/null +++ b/.superpowers/brainstorm/1528-1785381873/state/server.pid @@ -0,0 +1 @@ +1540 diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html new file mode 100644 index 0000000..8f73439 --- /dev/null +++ b/.superpowers/brainstorm/1612-1785939150/content/dashboard-layout.html @@ -0,0 +1,73 @@ +

实时轨迹规划网页看板:布局选择

+

三种方案都保持 OBSERVE_ONLY。蓝线表示当前规划,灰线表示上一轮规划;红点专门标记滚动交接处。

+ + + +
+
+
+
+
A · 总览 + 分析页签● cycle 128 · Rolling
+
+
世界路径 / 当前与上一轮
+
+
关键状态
v
0.18
a
0.04
j
-0.12
+
交接检查
速度连续 ✓
非零滚动终点 ✓
jerk 合规 ✓
+
+
+
LS / ST曲率v-a-j历史
+
L-S
T-S
T-V
+
+
+

A · 分层混合(推荐)

首屏保留路径、状态和异常;曲线按页签切换。信息完整,浏览器与车辆端的内存/重绘压力较平衡。

+
+ +
+
+
+
B · 六图同时显示● LIVE 20 Hz
+
+
世界路径
L-S
T-S
+
曲率-S
速度-T
加速度-T
+
加加速度-T
规划耗时
交接误差
+
+
cycle 128OSQP 42 ms0 violations
+
+
+

B · 密集工程屏

一次看到所有图,适合大显示器持续监控;小屏可读性较差,浏览器渲染负载最高。

+
+ +
+
+
+
C · 单图聚焦Rolling handoff #128
+
路径LSSTJerk-T
+
加加速度 / 限值带
+
当前轮
max |j| 0.31
交接点
Δa 0.02
上一轮末端
v 0.17
下一轮首点
v 0.18
+
+
+

C · 单图诊断

一次聚焦一条曲线,最轻量、适合笔记本;跨图对照需要切换,实时总览能力较弱。

+
+
+ +

请点击一个布局。我的建议是 A:默认总览轻量,发生异常时再切到曲率或 v-a-j 细节。

diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html new file mode 100644 index 0000000..17dd441 --- /dev/null +++ b/.superpowers/brainstorm/1612-1785939150/content/dashboard-scientific.html @@ -0,0 +1,70 @@ +

科研绘图风 · A 布局精修

+

以论文图表为基准:细线、白底、单位完整、图例克制;红色只表示真正越界。

+ + + +
+
Preview: Scientific trajectory dashboard
+
+
+
+
EM Trajectory Planning Observation
OBSERVE_ONLY · local session · coordinate units: m / s / rad
+
● LIVE   Cycle 128   RollingContinuation   42 ms
+
+
+
+
(a) World path and rolling handoff━ current   ┄ previous   ● handoff
+ + + + + + + + World X (m)World Y (m) + +
+
+
Current sample
t0.35 s
v0.182 m/s
a0.041 m/s²
j−0.118 m/s³
κ0.224 m⁻¹
+
Rolling continuity
terminal speed ≠ 0PASS
|Δv| ≤ tol0.003
|Δa| ≤ tol0.018
|j| ≤ limitPASS
trajectory age61 ms
+
+
+
LS / STCurvatureVelocity / Acceleration / JerkCycle history
+
+
(b) Lateral offset
s (m)l (m)
+
(c) Longitudinal progress
t (s)s (m)
+
(d) Signed velocity
t (s)v (m/s)
+
+
Refresh: 10 Hz · latest-snapshot transport · history: 60 cycles (bounded) · browser clients: 1 · no planner backpressure
+
+
+
+ +
+

采用这个科研风格

后续页面都沿用细线、白底、论文式多子图与单位标注。

+
diff --git a/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html b/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html new file mode 100644 index 0000000..0e060fd --- /dev/null +++ b/.superpowers/brainstorm/1612-1785939150/content/dashboard-style.html @@ -0,0 +1,74 @@ +

A 布局:选择视觉风格

+

图表颜色始终语义一致:当前轨迹为蓝色、上一轮为灰色、交接点为橙色、越界才使用红色。

+ + + +
+
+
+
轨迹规划观测台● 实时 · Cycle 128
+
世界路径
+
当前状态
v
0.18
a
0.04
j
-0.12
交接检查
✓ 非零滚动终点
✓ 速度/加速度连续
✓ Jerk 合规
+
LS / ST曲率v-a-j历史
L-S
T-S
T-V
+

1 · 清晰实验室(推荐)

浅色、低饱和、高可读性,适合长时间观察、截图和报告。

+
+ +
+
+
TRAJECTORY OBSERVATIONLIVE / 128
+
WORLD PATH
+
STATE
v
0.18
a
0.04
j
-0.12
HANDOFF
PASS / rolling
PASS / continuity
PASS / jerk
+
LS / STCURVATUREV-A-JHISTORY
L-S
T-S
T-V
+

2 · 工程图纸

米白与墨色,接近论文/仪器输出,克制但稍显传统。

+
+ +
+
+
轨迹规划观测台● 实时 · Cycle 128
+
世界路径
+
当前状态
v
0.18
a
0.04
j
-0.12
交接检查
✓ 非零滚动终点
✓ 速度/加速度连续
✓ Jerk 合规
+
LS / ST曲率v-a-j历史
L-S
T-S
T-V
+

3 · 石墨夜间

中性深灰、低炫光;比上一版更克制,适合暗光车间。

+
+
+ +

请点击一种风格;也可以回复“以 1 为基础,但……”。

diff --git a/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html b/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html new file mode 100644 index 0000000..093b38a --- /dev/null +++ b/.superpowers/brainstorm/1612-1785939150/content/waiting-architecture.html @@ -0,0 +1,6 @@ +
+
+

科研风格已确认

+

界面说明使用中文;坐标轴、单位和数学符号保留英文规范。继续在终端讨论数据服务架构。

+
+
diff --git a/.superpowers/brainstorm/1612-1785939150/state/server-stopped b/.superpowers/brainstorm/1612-1785939150/state/server-stopped new file mode 100644 index 0000000..41cca7d --- /dev/null +++ b/.superpowers/brainstorm/1612-1785939150/state/server-stopped @@ -0,0 +1 @@ +{"reason":"stop-server.sh","timestamp":1785940779} diff --git a/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html b/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html new file mode 100644 index 0000000..da7344e --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/diagram-strategy.html @@ -0,0 +1,54 @@ +

不同算法应该怎样被可视化?

+

请选择生成策略。重点考虑:既要自动化,也要让不同算法的关键机制真正容易理解。

+ +
+
+
+
A · 推荐:自适应图形族
+
+
数据处理函数
输入 → 变换 → 校验 → 输出
+
搜索/规划算法
起点 ⇢ 扩展 ⇢ 代价 ⇢ 目标
+
状态机
Idle ⇄ Running → Error
+
数值/几何算法
曲线、约束区间、异常峰值
+
+
+
+

根据算法特征自动选择主视图

+

所有类型共享同一诊断叠加协议,但主图可以是流程、搜索路径、状态转移或数值曲线。表达力最强,同时保留统一点击体验。

+
+
+ +
+
+
B · 通用流程图
+
+ 输入 + 步骤 A + 步骤 B ⚠ + 输出 +
+
+
+

所有函数统一画成节点流程

+

生成稳定、实现简单、读法一致;但数值曲线、状态跳转和搜索空间等算法特征会被压扁,可能仍停留在“文字方框”。

+
+
+ +
+
+
C · 人工指定视图
+
+
日报输入中指定:visual_type = curve
+
自定义节点、坐标、连线和交互说明
+
+
+
+

由 Agent 或用户提供专用图形描述

+

定制能力最高,但生成成本大、输入要求高,也更依赖当次 Agent 是否完整理解算法,难以保证每份日报都有图。

+
+
+
+ +
+

建议 A:使用“统一诊断协议 + 自适应主视图”。不管图形类型如何变化,点击节点后都固定展示源码、当前状态、原因、后果、修正方案和预期结果。

+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html b/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html new file mode 100644 index 0000000..52a45c4 --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/domain-planning-visual-v2.html @@ -0,0 +1,89 @@ +

修订方向:展示算法的真实领域效果

+

下面以泊车路径规划为例。主画面直接呈现规划场景与算法结果,流程和文字只负责解释“为什么”。

+ +
+
路径规划诊断 · Parking Scenario #08
+
+
+ 正常机制 + ● 当前问题 + ○ 候选修正预演 + ✓ 已验证结果 + 播放搜索 逐步执行 重置视角 +
+ +
+
+ + + + + + + + + + + + + + + 目标车位 + + + + + + 起始姿态 + + + + + + + + + + 最小间距失效 + 车体包络进入障碍膨胀区 + + + + 当前轨迹 + 修正预演 + + + +
+
环境建模
+
状态扩展
+
碰撞检查 ⚠
+
路径平滑
+
安全验证
+
+
+ + +
+
+
+ +
+
+

就是这个方向

算法主区域展示真实业务对象和运行结果,诊断信息与源码证据随点击联动。

+
+
+

还需要调整

保留领域可视化方向,但需要改变内容层次或交互方式。

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html b/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html new file mode 100644 index 0000000..f87a7ed --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/interaction-design-v3.html @@ -0,0 +1,72 @@ +

交互设计:从算法效果一路追到验证结果

+

示意的是所有领域共用的操作方式;中央主画面会根据算法自动变成路径、曲线、搜索树、状态机或其他实际效果图。

+ +
+
+ 算法诊断报告算法:路径规划器 ▾ 问题:最小间距失效 ▾ +
+
+
+ + +
+
+ 领域效果主画面播放 暂停 单步 缩放 重置 +
+
+
+
输入/起点
+
输出/目标
+ + + + + + +
点击异常对象
主画面与右侧诊断联动
+
+
+
输入阶段
核心计算
异常阶段 ⚠
输出验证
+
+
+ + +
+ +
+
一次完整点击路径
+

选择算法 → 查看正常机制 → 点击异常对象 → 播放影响传播 → 逐步预演纠正方案 → 切换已验证结果

+
+
+
+ +
+
+

确认这套交互

领域主画面、四种状态、对象诊断卡和证据下钻共同组成报告页面。

+
+
+

需要调整

说明希望改变的交互顺序、页面区域或信息层次。

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html b/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html new file mode 100644 index 0000000..052ec8a --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/interaction-layout.html @@ -0,0 +1,69 @@ +

选择算法诊断页的交互结构

+

三种方案都能展示“正常功能 → 当前问题 → 原因 → 后果 → 纠正方案 → 预期结果”。请重点比较:第一眼是否容易理解,以及点击后能否逐层深入。

+ +
+
+
+
B · 推荐:双层算法地图
+
算法基线 | 问题叠加 | 修正预演  [当前:问题叠加]
+
+
+ 输入路径 + 曲率计算 ⚠ + 平滑约束 + 输出轨迹 +
+
+
原因
边界导数不连续
+
影响传播
曲率峰值 → 控制抖动
+
纠正预演
新约束 → 预期稳定
+
+
+
+
+

基线流程 + 故障/修正叠加层

+

先固定展示算法正常机制;点击问题后在同一流程上高亮故障节点、原因和影响,再切换到修正方案预演。上下文不丢失,最适合函数与算法诊断。

+
+
+ +
+
+
A · 线性故事链
+
+
1. 原算法功能 输入如何变成输出
+
↓ 点击继续
+
2. 当前问题 异常发生在哪里
+
+
3. 原因与后果 为什么错、会导致什么
+
+
4. 纠正与预期 改什么、预计变成什么
+
+
+
+

从上到下的诊断故事

+

阅读顺序最直观,适合汇报展示;但算法复杂或问题较多时页面会变长,比较多个问题时容易失去原流程位置。

+
+
+ +
+
+
C · 自由探索工作台
+
+
函数列表

calculate()

smooth()

validate()
+
+
可缩放算法关系图
节点可自由点击
+
+
+
节点详情
输入
源码位置
问题
根因
影响
验证
+
+
+
+

图谱式诊断工作台

+

探索能力最强,适合大型算法和多个函数;但实现与维护成本最高,日报第一次打开时也更容易让读者迷失。

+
+
+
+ +
+

我的建议:选择 B。它保留原算法作为稳定“地图”,问题和解决方案都是同一张地图上的可切换叠加层,最符合你强调的“点击就能理解目前状况和修正后结果”。

+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html new file mode 100644 index 0000000..4ae751c --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/waiting-architecture.html @@ -0,0 +1,6 @@ +
+
+

领域算法可视化方向已确认

+

正在终端中比较可复用的实现架构……

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html new file mode 100644 index 0000000..4339a62 --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/waiting-evidence-state.html @@ -0,0 +1,6 @@ +
+
+

已确定:混合粒度算法图

+

主图展示算法阶段,节点可下钻至函数与源码。正在终端中确认修正预演的证据状态……

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html new file mode 100644 index 0000000..ec9883f --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/waiting-granularity.html @@ -0,0 +1,6 @@ +
+
+

已选择:B · 双层算法地图

+

正在终端中确认算法节点的组织粒度……

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html b/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html new file mode 100644 index 0000000..7e033f6 --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/content/waiting-validation.html @@ -0,0 +1,6 @@ +
+
+

交互设计已确认

+

正在终端中确认降级规则与验收标准……

+
+
diff --git a/.superpowers/brainstorm/1923-1785734653/state/server-stopped b/.superpowers/brainstorm/1923-1785734653/state/server-stopped new file mode 100644 index 0000000..60a2089 --- /dev/null +++ b/.superpowers/brainstorm/1923-1785734653/state/server-stopped @@ -0,0 +1 @@ +{"reason":"stop-server.sh","timestamp":1785737557} diff --git a/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html b/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html new file mode 100644 index 0000000..dc768c2 --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/content/dashboard-layout-options.html @@ -0,0 +1,10 @@ + +

网页主界面布局选择

+

三种方案都包含独立页签、X/Y 数值刻度与单位、图例、图层显隐、框选/滚轮缩放、复位及单图全屏。

+
+

A · 地图优先

大地图、右侧状态、底部单张分析图。

完整方向段 · OBSERVE_ONLY图层 复位
+ − ⛶
终点 3 cm / 5°
v=0 · a=0
制动点 S=6.4m
框选 复位 ⛶完整 ST
路径 LS/ST 运动学 诊断
+
推荐

B · 地图与分析并列

左侧完整路径,右侧同时看选中图表和终点状态。

段 0 · Forward · 完整规划成功图层 导出
+ − ⛶
框选 ⛶完整 ST:起步—巡航—停车
目标速度 1.0m/s
终点误差合格
无零进度异常
路径 LS/ST 曲率 速度/加速度/jerk 诊断
+

C · 多图分析墙

地图在上,LS、ST、速度三图始终并排。

完整方向段分析同步缩放 复位全部
+ − ⛶
LS
ST
速度
基础图组 运动学 终点验证 配置
+
diff --git a/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html b/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html new file mode 100644 index 0000000..27a05be --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/content/dashboard-paper-thin-v2.html @@ -0,0 +1,23 @@ + +

论文风格 · 细线版

+

恢复之前的科研绘图语言,并进一步减小所有线宽。蓝色只表示完整规划轨迹,红色只表示真实越界。

+
Preview: Full-direction-segment trajectory observation
+
+
EM Full-Direction-Segment Trajectory Observation
OBSERVE_ONLY · segment 0 / Forward · units: m, s, rad
● VALID FullDirectionSegment solve 186 ms
+
+
(a) World path and complete planned trajectoryLayers Box zoom Reset Full screen
+ World X (m)World Y (m)brake start +
+
Terminal boundary
Δ position0.012 m
Δ yaw1.8°
v terminal0.000 m/s
a terminal0.000 m/s²
yaw rate0.000 rad/s
Profile summary
segment length8.42 m
planned duration12.80 s
cruise target1.00 m/s
brake start6.31 m
zero progressNO
+
+
LS / STCurvatureVelocity / Acceleration / JerkTerminal validationConfiguration
+
+
(b) Lateral offsetZoom Reset ⛶
ReferenceS (m)l (m)
+
(c) Complete STZoom Reset ⛶
t (s)PathS (m)
+
(d) Signed velocityZoom Reset ⛶
t (s)v (m/s)
+
+
Thin scientific rendering · current trajectory 0.70 px · reference 0.50 px · grid 0.35 px · all axes include numeric ticks and units · per-plot zoom does not alter source data
+
+

采用论文风格细线版

后续页面沿用这套排版、线宽、颜色和交互密度。

diff --git a/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html b/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html new file mode 100644 index 0000000..ea56516 --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/content/waiting-targeted-repair.html @@ -0,0 +1,6 @@ +
+
+

保留原页面与论文风格

+

当前设计已收紧为:修复空白、页签、数据、刻度和图层缺陷;补充 s_end、换向点、车辆位置与路径对比,不重做布局。

+
+
diff --git a/.superpowers/brainstorm/985-1786008552/state/server-instance-id b/.superpowers/brainstorm/985-1786008552/state/server-instance-id new file mode 100644 index 0000000..0a6049c --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/state/server-instance-id @@ -0,0 +1 @@ +df42eac72602791b7f917cb1b2703cca549757ea1ad9eca5 diff --git a/.superpowers/brainstorm/985-1786008552/state/server-stopped b/.superpowers/brainstorm/985-1786008552/state/server-stopped new file mode 100644 index 0000000..ecded91 --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1786050319918} diff --git a/.superpowers/brainstorm/985-1786008552/state/server.pid b/.superpowers/brainstorm/985-1786008552/state/server.pid new file mode 100644 index 0000000..eeb072e --- /dev/null +++ b/.superpowers/brainstorm/985-1786008552/state/server.pid @@ -0,0 +1 @@ +997 diff --git a/.task8-sweep/AGV.cs b/.task8-sweep/AGV.cs new file mode 100644 index 0000000..b0c86c0 --- /dev/null +++ b/.task8-sweep/AGV.cs @@ -0,0 +1,534 @@ +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Sensors; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using FundamentalLib; +using MDCSToolBox.Clumsy.AgvInterfaces; +using MDCSToolBox.Clumsy.Calibration; +using MDCSToolBox.Clumsy.MotionControllers; +using MDCSToolBox.Clumsy.Tracks; +using MDCSToolBox.Commons.Controllers; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Numerics; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using static ClumsyCore.DTools.Painter; + +namespace MultiWheelC +{ + public class SetLocationRes + { + public float x, y, th; + public int l_step; + public long tick; + public string error; + } + public class AGV : MultiWheelInterface + { + public override AbstractGeometricController GetController() + { + return new ChassisController().Get(); + } + + public override MultiWheelMagTracker GetMagController() + { + return new MultiWheelMagTracker(); + } + + public override NaiveMagnetController GetNaiveMagnetController() + { + return new NaiveMagnetController(); + } + + public void Sleep(float s) + { + new DriveTask(new Sleep() { Second = s }.Get()).Wait(); + } + + public void ControlChargePort(bool open) + { + DLog.Log($"call ControlChargePort({open})"); + PilotDefinition.Self.OpenChargeByClumsy = open; + } + + public void SwitchLidarArea(int area) + { + DLog.Log($"call SwitchLidarArea({area})"); + PilotDefinition.Self.AreaChoose = area; + } + + public void SwitchIoArea(int area) + { + if (area != -1) + { + PilotDefinition.Self.IOObstacleArea = area; + } + } + public void RotateToTarget(float target) + { + //if (!needrotate) return; + var dl = new DriveTask(new MultiWheelRotateInPlace() + { + AngleTarget = target, + PidparamsRead = () => new PIDParams() + { + Kp = PilotDefinition.Conf.TireFollowingThkp, + Ki = PilotDefinition.Conf.TireFollowingThki, + Kd = PilotDefinition.Conf.TireFollowingThkd, + DeadZone = PilotDefinition.Conf.TireFollowingThDeadZone, + SpeedAccPerSec = PilotDefinition.Conf.TireFollowingThSpeedAccPerSec, + OutputUpperThreshold = PilotDefinition.Conf.TireFollowingThThresh, + MaxI = PilotDefinition.Conf.TireFollowingThMaxI, + } + }.Get()); + dl.Wait(); + } + + //1tireNum Ҫ̥ + //2frontLidarDetect true:ǰ״ʶ false:״ʶ + public void TireFollowing(int tireNum, bool frontLidarDetect, int srcId, int dstId) + { + while (!TryLock(dstId)) + { + Thread.Sleep(50); + } + DLog.Log($"{dstId}", "TireFollowing"); + var lidarName = frontLidarDetect ? "ǰ״" : "״"; + DLog.Log($"ʼ공ͨ{lidarName}ʶ{tireNum}̥", "TireFollowing"); + if (tireNum != 1 && tireNum != 2) + { + DLog.Log($"TireNum12 (ǰ:{tireNum})", "TireFollowing"); + return; + } + + if (PilotDefinition.Self.GhostMode) + { + while (!TryLock(dstId)) + { + Console.WriteLine("ȴȡ..."); + Thread.Sleep(200); + } + Console.WriteLine($"{dstId}"); + Thread.Sleep(1000); + Console.WriteLine($"ʼ공ͨ{lidarName}ʶ{tireNum}̥"); + Thread.Sleep(1000); + Leave(srcId); + Console.WriteLine($"ʼһäߣʱͷԤȡ{srcId}"); + Thread.Sleep(2000); + //Leave(dstId); + //Console.WriteLine($"һäߣʱͷȡ{dstId}"); + Thread.Sleep(2000); + Console.WriteLine($"공"); + return; + } + + var detectors = new List() + { + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, frontLidarDetect), + StartGuessingX = frontLidarDetect ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, + frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0), + LeaveSrcFunction = Leave, + SrcId = srcId, + DstId = dstId, + }, + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, frontLidarDetect), + StartGuessingX = frontLidarDetect ? PilotDefinition.Conf.TireFollowingStage2GuessX : -PilotDefinition.Conf.TireFollowingStage2GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, + frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0) + }, + }; + DLog.Log($"̥Ϊ{tireNum}", "TireFollowing"); + var following = new TireFollowing() + { + GetController = () => new ChassisController().Get(), + GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, + GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, + detectors = detectors, + SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, + MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, + TireNum = tireNum, + CarDirection = frontLidarDetect ? 0f : 180f, + WalkBlindTh = frontLidarDetect ? PilotDefinition.Conf.TireFollowingFrontLidarWalkBlindTh : PilotDefinition.Conf.TireFollowingBackLidarWalkBlindTh, + }; + var _dt = new DriveTask(following.Get()); + _dt.Wait(); + DLog.Log("공", "TireFollowing"); + } + + //복һǺ״ʶһ̥ + public void LeaveCar(int srcId, float srcX, float srcY, int dstId, float dstX, float dstY) + { + while (!TryLock(dstId)) + { + Thread.Sleep(50); + } + DLog.Log($"{dstId}", "TireFollowing"); + DLog.Log($"ʼ공ͨ״ʶ1̥", "TireFollowing"); + var chassis = (MultiWheelChassis)PilotDefinition.Chassis; + chassis.SetOriginBias(0, 0, 0); + var following = new TireFollowing() + { + GetController = () => new ChassisController().Get(), + GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, + GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, + detectors = new List() + { + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), + StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingLeaveCarWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, + PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0), + LeaveSrcFunction = Leave, + SrcId = srcId, + DstId = dstId, + }, + }, + CarDirection = 180f, + SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, + MaxSpeed = 0.25f, + EnableHandover = true, + HandoverDistance = 200f, + HandoverSpeed = 0.3f, + WalkBlindTh = 0, + TireNum = 1 + }; + + IEnumerable LeaveThenFollow() + { + foreach (var running in following.Get()) + { + if (!running) break; + yield return true; + } + DLog.Log($"ͷ{srcId}", "TireFollowing"); + DLog.Log("복TireFollowingʼDstTracker", "TireFollowing"); + + foreach (var running in new DstTracker() + { + Src = new Vector2(srcX, srcY), + Dst = new Vector2(dstX, dstY), + CarDirectionBias = 180f, + InitialSendSpeed = 0.3f + }.Get()) + { + if (!running) break; + yield return true; + } + + yield return false; + } + + var _dt = new DriveTask(LeaveThenFollow()); + _dt.Wait(); + DLog.Log("복1", "TireFollowing"); + } + + public void LineTracking(int srcId, float srcX, float srcY, int dstId, float dstX, float dstY) + { + while (!TryLock(dstId)) + { + Thread.Sleep(50); + } + var chassis = (MultiWheelChassis)PilotDefinition.Chassis; + chassis.SetOriginBias(0, 0, 0); + DLog.Log($"{dstId}", "TireFollowing"); + IEnumerable TrackThenFollow() + { + foreach (var running in new LineTracking() + { + Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, + LeaveSrcFunction = Leave, + SrcId = srcId, + EnableHandover = true, + HandoverDistance = 200, + HandoverSpeed = 0.3f, + }.Get()) + { + if (!running) break; + yield return true; + } + DLog.Log($"ͷ{srcId}", "TireFollowing"); + DLog.Log("복LineTrackingʼDstTracker", "TireFollowing"); + while (!TryLock(426)) + { + Thread.Sleep(20); + } + Leave(dstId); + DLog.Log($"ͷ{dstId}", "TireFollowing"); + foreach (var running in new DstTracker() + { + Src = new Vector2(srcX, srcY), + Dst = new Vector2(dstX, dstY), + InitialSendSpeed = 0.3f + }.Get()) + { + if (!running) break; + yield return true; + } + yield return false; + } + var _dt = new DriveTask(TrackThenFollow()); + _dt.Wait(); + DLog.Log("복2", "TireFollowing"); + } + + //ʹ + public void DriverAble() + { + var dl = new DriveTask(new DriverAble() { }.Get()); + dl.Wait(); + DLog.Log("ʹ", "TireFollowing"); + } + + //ʹ + public void DriverDisable() + { + var dl = new DriveTask(new DriverDisable() { }.Get()); + dl.Wait(); + DLog.Log("ʹ", "TireFollowing"); + } + + // бclose Ϊ true ʱرռб򿪼б + public void ClamptoTarget(bool close) + { + if (PilotDefinition.Self.GhostMode) + { + Thread.Sleep(2000); + Console.WriteLine("б"); + return; + } + new DriveTask(new ClampToTarget() + { + LeftClampTarget = close ? PilotDefinition.Self.LeftArmUpperPos : PilotDefinition.Self.LeftArmLowerPos, + RightClampTarget = close ? PilotDefinition.Self.RightArmUpperPos : PilotDefinition.Self.RightArmLowerPos + }.Get()).Wait(); + } + // Fleet crab walk: convert scheduler src/dst into the same relative crab-walk path used by MovementTest. + public void FleetCrabWalk(float srcX, float srcY, int srcId, float dstX, float dstY, int dstId, + float speed) + { + var dx = dstX - srcX; + var dy = dstY - srcY; + var pathLength = (float)Math.Sqrt(dx * dx + dy * dy); + + if (pathLength <= 1f) + { + DLog.Log("FleetCrabWalk abort: path length is too short.", "FleetCrabDbg"); + return; + } + + var self = PilotDefinition.Self; + if (!self.TryGetFleetCenterFromMembers(out var centerX, out var centerY, out var centerTh) && + !self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) + { + DLog.Log("FleetCrabWalk abort: failed to read fleet center.", "FleetCrabDbg"); + Hedingben.ToastText("FleetCrab requires master localization", "FleetCrab"); + return; + } + + var pathAngle = (float)CommonMath.RoundTh((float)(Math.Atan2(dy, dx) / Math.PI * 180.0)); + var crabAngle = (float)CommonMath.ThDiff(pathAngle, centerTh); + var targetBodyWorldHeading = (float)CommonMath.RoundTh(PilotDefinition.Conf.FleetCrabBodyWorldHeadingDeg); + var bodyToPathAngle = (float)CommonMath.ThDiff(pathAngle, targetBodyWorldHeading); + DLog.Log( + $"call FleetCrabWalk(src=({srcX:0},{srcY:0},id:{srcId}), dst=({dstX:0},{dstY:0},id:{dstId}), " + + $"len={pathLength:0.0}, speed={speed:0.000}, pathAngle={pathAngle:0.0}, " + + $"center=({centerX:0},{centerY:0},{centerTh:0.0}), crabAngle={crabAngle:0.0}, " + + $"targetBodyWorld={targetBodyWorldHeading:0.0}, bodyToPath={bodyToPathAngle:0.0})", + "FleetCrabDbg"); + + if (dstId != -1) + { + while (!TryLock(dstId)) + { + Thread.Sleep(50); + } + DLog.Log($"{dstId}", "FleetCrabDbg"); + } + + var action = new MultiWheelC.FleetCrabWalk + { + CrabAngleDeg = crabAngle, + BodyToPathAngleDeg = bodyToPathAngle, + CrabLengthMm = pathLength, + CrabSpeed = speed, + FleetCrabAccel = PilotDefinition.Conf.FleetCrabAccel, + FleetCrabStartAccel = PilotDefinition.Conf.FleetCrabStartAccel, + FleetCrabSlowDistance = PilotDefinition.Conf.FleetCrabSlowDistance, + FleetCrabFinishDistance = PilotDefinition.Conf.FleetCrabFinishDistance, + FleetCrabFinishSpeed = PilotDefinition.Conf.FleetCrabFinishSpeed, + FleetCrabSlowingPow = PilotDefinition.Conf.FleetCrabSlowingPow, + GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold + }; + + try + { + new DriveTask(action.Get()).Wait(); + } + finally + { + if (srcId != -1) + { + Leave(srcId); + DLog.Log($"ͷŷų{srcId}", "FleetCrabDbg"); + } + } + } + + public void FleetCurveWalk(float srcX, float srcY, int srcId, float dstX, float dstY, int dstId, + float speed, params float[] trackTypeInfo) + { + if (trackTypeInfo == null || trackTypeInfo.Length < 2) + { + DLog.Log("FleetCurveWalk abort: invalid trackTypeInfo, expected Bezier type info.", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve invalid trackTypeInfo", "FleetCurve"); + return; + } + + var trackType = (int)trackTypeInfo[0]; + if (trackType != 2) + { + DLog.Log($"FleetCurveWalk abort: unsupported trackType={trackType}, only Bezier(type=2) is supported.", + "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve only supports Bezier trackType=2", "FleetCurve"); + return; + } + + var controlPointNum = (int)trackTypeInfo[1]; + var expectedLength = 2 + controlPointNum * 2; + if (controlPointNum < 3 || trackTypeInfo.Length < expectedLength) + { + DLog.Log( + $"FleetCurveWalk abort: invalid Bezier trackTypeInfo. controlPointNum={controlPointNum}, " + + $"length={trackTypeInfo.Length}, expected>={expectedLength}.", + "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve invalid Bezier trackTypeInfo", "FleetCurve"); + return; + } + + BezierTrack track; + try + { + track = ProcessTrackTypeInfo(srcX, srcY, dstX, dstY, trackTypeInfo) as BezierTrack; + } + catch (Exception ex) + { + DLog.Log($"FleetCurveWalk abort: failed to process trackTypeInfo. {ex.Message}", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve failed to process track", "FleetCurve"); + return; + } + + if (track == null) + { + DLog.Log("FleetCurveWalk abort: ProcessTrackTypeInfo did not return BezierTrack.", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve requires BezierTrack", "FleetCurve"); + return; + } + + track.Speed = speed; + track.CarDirectionBias = 0f; + + DLog.Log( + $"call FleetCurveWalk(src=({srcX:0},{srcY:0},id:{srcId}), dst=({dstX:0},{dstY:0},id:{dstId}), " + + $"speed={speed:0.000}, trackType={trackType}, controls={controlPointNum}, track={track.GetType().Name}, " + + $"carDirectionBias=0.0)", + "FleetCurveDbg"); + + if (dstId != -1) + { + while (!TryLock(dstId)) + { + Thread.Sleep(50); + } + DLog.Log($"锁点{dstId}完成", "FleetCurveDbg"); + } + + var action = new MultiWheelC.FleetCurveWalk + { + Track = track, + CurveSpeed = speed, + CarDirectionBias = 0f, + SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance, + FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance, + FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed, + SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow, + GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold, + StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec + }; + + try + { + new DriveTask(action.Get()).Wait(); + } + finally + { + if (srcId != -1) + { + Leave(srcId); + DLog.Log($"release srcId={srcId}", "FleetCurveDbg"); + } + } + } + + public void ChangeAvoidanceDistance(float stopDistance, float slowDistance) + { + DLog.Log($"call ChangeAvoidanceDistance({stopDistance},{slowDistance})"); + PilotDefinition.Self.SlowDistance = slowDistance; + PilotDefinition.Self.StopDistance = stopDistance; + } + public void ChangeAvoidanceParam(float length = -1, float width = -1) + { + + PilotDefinition.Self.CarLength = length; + PilotDefinition.Self.CarWidth = width; + + } + public void SetLocation(float x, float y, float th) + { + DLog.Log($"call SetLocation({x},{y},{th})"); + Console.WriteLine($"call SetLocation({x},{y},{th})"); + Queue(() => + { + while (true) + { + var str1 = new HttpClient() + .GetStringAsync( + $"http://127.0.0.1:4321/setLocation?x={x}&y={y}&th={th}") + .Result; + Thread.Sleep(500); + Console.WriteLine($"SetLocation str={str1}"); + var setLocationRes = JsonConvert.DeserializeObject(str1); + Console.WriteLine(setLocationRes.l_step); + if (setLocationRes != null && setLocationRes.l_step == 2) break; + } + }); + } + public float baseSpeed = 0; + } +} diff --git a/.task8-sweep/ChassisController.cs b/.task8-sweep/ChassisController.cs new file mode 100644 index 0000000..c041b2a --- /dev/null +++ b/.task8-sweep/ChassisController.cs @@ -0,0 +1,100 @@ +using System; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using FundamentalLib; +using MDCSToolBox.Clumsy.MotionControllers; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; + +namespace MultiWheelC; + +public class ChassisController : MovementDefinition +{ + public float BaseSpeed = Configuration.conf.basicSpeed; + + private DateTime _sendMotionDbgLast = DateTime.MinValue; + + public override MultiWheelGeometricController Get() + { + return new MultiWheelGeometricController + { + Chassis = BasicPilotBase.Chassis, + BaseSpeed = BaseSpeed, + SlowDistance = PilotDefinition.Conf.SlowDistance, + SlowingPow = PilotDefinition.Conf.SlowingPow, + FinishDistance = PilotDefinition.Conf.FinishDistance, + FinishSpeed = PilotDefinition.Conf.FinishSpeed, + FirstThAccuracy = PilotDefinition.Conf.FirstThAccuracy, + FirstRotateSpeedFac = PilotDefinition.Conf.FirstRotateSpeedFac, + FirstRotateMaxSpeed = PilotDefinition.Conf.FirstRotateMaxSpeed, + NotContinuousAngle = PilotDefinition.Conf.NotContinuousAngle, + DebugMode = PilotDefinition.Conf.MotionDebugPrint, + DebugCurvature = PilotDefinition.Conf.DebugCurvature, + PowerSteeringLookAhead = PilotDefinition.Conf.PowerSteeringLookAhead, + SpeedLookAhead = PilotDefinition.Conf.SpeedLookAhead, + SpeedLookAheadCurveDiff = PilotDefinition.Conf.SpeedLookAheadCurveDiff, + SpeedLookBackCurveDiff = PilotDefinition.Conf.SpeedLookBackCurveDiff, + SpeedLimitCurveDiffMin = PilotDefinition.Conf.SpeedLimitCurveDiffMin, + SpeedLimitCurveMin = PilotDefinition.Conf.SpeedLimitCurveMin, + MaxRotateSpeed = PilotDefinition.Conf.MaxRotateSpeedCurveLimit, + MaxRotateAcc = PilotDefinition.Conf.MaxRotateAccCurveLimit, + GcpThetaThreshold = PilotDefinition.Conf.GcpThetaThreshold, + DthLinearFac = PilotDefinition.Conf.DthLinearFac, + DthLinearThreshold = PilotDefinition.Conf.DthLinearThreshold, + BiasFac = PilotDefinition.Conf.BiasFac, + BiasThreshold = PilotDefinition.Conf.BiasThreshold, + + MultiVehicleSendMotion = (speed, frontTh, rearTh, idealPos, idealAngle) => + { + var self = PilotDefinition.Self; + self.MultiVehicleAutoEnabled = true; + // A: 用固定锁对象(不再锁会被替换的字段引用)。 + int fleetCnt; + lock (self.FleetLock) + fleetCnt = self.MultiVehicleFleet.Count; + + // 诊断(节流 ~300ms):确认回调被调用、编队是否就绪、是否因数量不符提前 return(导致不下发速度)。 + if ((DateTime.Now - _sendMotionDbgLast).TotalMilliseconds >= 300) + { + _sendMotionDbgLast = DateTime.Now; + DLog.Log( + $"SENDMOTION speed={speed:0.000} fTh={frontTh:0.0} rTh={rearTh:0.0} " + + $"ideal=({idealPos.X:0},{idealPos.Y:0},{idealAngle:0.0}) " + + $"editCnt={fleetCnt}/{PilotDefinition.Conf.MultiVehicleFleetNum} " + + $"earlyReturn={fleetCnt != PilotDefinition.Conf.MultiVehicleFleetNum}", + "FleetCrabDbg"); + } + + if (fleetCnt != PilotDefinition.Conf.MultiVehicleFleetNum) + return; + + self.MultiVehicleAutoVx = speed; + self.MultiVehicleAutoFrontTh = frontTh; + self.MultiVehicleAutoRearTh = rearTh; + // D: 透传路径控制器算出的理想车队中心位姿(此前被丢弃),供各车按 layout 做前馈。 + self.MultiVehicleAutoIdealX = idealPos.X; + self.MultiVehicleAutoIdealY = idealPos.Y; + self.MultiVehicleAutoIdealTh = idealAngle; + self.MultiVehicleAutoHasIdeal = true; + // B: 标记命令新鲜度。路径结束/早退/卡顿不再刷新此时刻 → 主车超时后清零速度,避免滑行。 + self.MultiVehicleAutoCmdTime = DateTime.Now; + }, + + // G: 读取车队中心原子快照,避免跨线程读到撕裂的 x/y/th 组合。 + MultiVehicleGetFleetPos = () => + { + var snap = PilotDefinition.Self.GetFleetCenterSnapshot(); + return new Location + { + x = snap.X, + y = snap.Y, + th = snap.Th, + l_step = 1, + tick = DateTime.Now.Ticks + }; + } + }; + } +} diff --git a/.task8-sweep/ClumsyPilot.csproj b/.task8-sweep/ClumsyPilot.csproj new file mode 100644 index 0000000..29039db --- /dev/null +++ b/.task8-sweep/ClumsyPilot.csproj @@ -0,0 +1,53 @@ + + + + netstandard2.0 + 10 + true + + + + + + + + + + + + + + + + ref\CommonUsage.dll + + + ref\LessokajiWeaverUtilities.dll + + + ref\MDCSToolBox.dll + + + ref\RefClumsyCore.dll + + + ref\RefClumsyDance.dll + + + ref\RefFundamentalLib.dll + + + + + + + + + + + + diff --git a/.task8-sweep/FleetCrabWalk.cs b/.task8-sweep/FleetCrabWalk.cs new file mode 100644 index 0000000..c75333d --- /dev/null +++ b/.task8-sweep/FleetCrabWalk.cs @@ -0,0 +1,526 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using FundamentalLib; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; + +namespace MultiWheelC; + +// ===== 车队联动-自动蟹行动作 ===== +// 以当前车队中心为起点,构造指定方向和长度的直线路径; +// 执行侧直接写 MultiVehicleAuto...,由 TickMultiVehicle 自动分支统一下发。 +// +// 控制思路参考 MDCSToolbox 几何控制器,但实现收在 MultiWheelC 内: +// 1) 读取主车 Detour 反推车队中心,计算沿直线的进度、横向偏差和车身目标朝向偏差; +// 2) 根据横向偏差给前后 GCP 同向修正,根据车身目标朝向偏差给前后 GCP 反向修正; +// 3) 根据终点距离减速,并发布 ideal fleet center 给从车做前馈。 +// +// 前提:在主车(MultiVehicleMasterEndpoint=="/")运行,且主车有 Detour 定位。 +public class FleetCrabWalk : MovementDefinition +{ + /// 路径方向相对启动时车队朝向的夹角(deg,逆时针为正)。 + public float CrabAngleDeg = 45f; + + /// 路径方向相对车身目标朝向的夹角(deg,逆时针为正)。MovementTest 会设为 CrabAngleDeg,以保持启动时车身朝向。 + public float BodyToPathAngleDeg = 45f; + + /// 路径长度(mm)。 + public float CrabLengthMm = 2000f; + + /// 行驶速度(m/s)。 + public float CrabSpeed = 0.2f; + + /// 速度命令加速度限制(m/s^2),小于等于 0 表示不限制。 + public float FleetCrabAccel = 0.2f; + + /// 预对齐后正式下发速度前 5 秒加速度限制(m/s^2),小于等于 0 表示不限制。 + public float FleetCrabStartAccel = 0.01f; + + /// 末端开始减速距离(mm)。 + public float FleetCrabSlowDistance = 2000f; + + /// 完成距离(mm),低于该剩余距离结束动作。 + public float FleetCrabFinishDistance = 20f; + + /// 末端最低速度(m/s)。 + public float FleetCrabFinishSpeed = 0.02f; + + /// 末端减速曲线指数。 + public float FleetCrabSlowingPow = 0.8f; + + /// 前后 GCP 舵角修正上限(deg)。 + public float GcpThetaThreshold = 95f; + + private bool _stopping; + + private void Cleanup() + { + var self = PilotDefinition.Self; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = 0; + self.MultiVehicleAutoRearTh = 0; + self.MultiVehicleAutoHasIdeal = false; + self.MultiVehicleAutoEnabled = false; + } + + public void Stop() + { + _stopping = true; + Cleanup(); + } + + private static float Clamp(float value, float min, float max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + private static float ClampAbs(float value, float limit) + { + var absLimit = Math.Abs(limit); + if (absLimit <= 0) return value; + if (value > absLimit) return absLimit; + if (value < -absLimit) return -absLimit; + return value; + } + + private static float Slew(float current, float target, float maxDelta) + { + if (maxDelta <= 0) return target; + if (target > current + maxDelta) return current + maxDelta; + if (target < current - maxDelta) return current - maxDelta; + return target; + } + + private static float AverageAngle(float frontTh, float rearTh) + { + var diff = (float)CommonMath.ThDiff(frontTh, rearTh); + return (float)CommonMath.RoundTh(rearTh + diff / 2f); + } + + private static void ResolveCrabDriveEquivalent(float speed, float rawFrontTh, float rawRearTh, float steerLimit, + out float driveSpeed, out float frontTh, out float rearTh, out bool reverseEquivalent, out float rawBaseTh) + { + var limit = Math.Min(179f, Math.Max(1f, Math.Abs(steerLimit))); + rawBaseTh = AverageAngle(rawFrontTh, rawRearTh); + driveSpeed = speed; + frontTh = rawFrontTh; + rearTh = rawRearTh; + reverseEquivalent = false; + + if (rawBaseTh > limit) + { + frontTh = (float)CommonMath.RoundTh(frontTh - 180f); + rearTh = (float)CommonMath.RoundTh(rearTh - 180f); + driveSpeed = -driveSpeed; + reverseEquivalent = true; + } + else if (rawBaseTh < -limit) + { + frontTh = (float)CommonMath.RoundTh(frontTh + 180f); + rearTh = (float)CommonMath.RoundTh(rearTh + 180f); + driveSpeed = -driveSpeed; + reverseEquivalent = true; + } + + frontTh = ClampAbs(frontTh, limit); + rearTh = ClampAbs(rearTh, limit); + } + + private static float ProbeSpeed(float speed) + { + return Math.Abs(speed) > 1e-4f ? speed : 1f; + } + + private static bool TryGetMotionYawSign(float frontTh, float rearTh, float driveSpeed, float controlRadius, + out float yawSign) + { + yawSign = 0f; + if (Math.Abs(CommonMath.ThDiff(frontTh, rearTh)) <= 1e-3f) + return false; + + var radius = Math.Max(1f, Math.Abs(controlRadius)); + Vector2 pFront = new(radius, 0), pRear = new(-radius, 0), + normFront = CommonMath.Transform2D(pFront, frontTh + 90f, Vector2.UnitX), + normRear = CommonMath.Transform2D(pRear, rearTh + 90f, Vector2.UnitX); + var (intersect, center) = CommonMath.TwoLinesIntersection(pFront, normFront, pRear, normRear); + if (!intersect) + return false; + + // Match MultiWheelChassis.SendMotion: the tangent side is selected by + // rotCenter.Y > 1, and reverse-equivalent motion flips the yaw direction. + var tangentSign = center.Y > 1f ? 1f : -1f; + var speedSign = driveSpeed >= 0f ? 1f : -1f; + yawSign = speedSign * tangentSign; + return true; + } + + private static float GetYawSplitSign(float baseTh, float speed, float steerLimit, float controlRadius) + { + const float probeDth = 1f; + ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + probeDth, baseTh - probeDth, steerLimit, + out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _); + return TryGetMotionYawSign(probeFrontTh, probeRearTh, probeSpeed, controlRadius, out var yawSign) + ? yawSign + : 1f; + } + + private static float EstimateLateralVelocity(float bodyTh, float frontTh, float rearTh, float driveSpeed, + Vector2 pathLeft) + { + var motionTh = (float)CommonMath.RoundTh(bodyTh + AverageAngle(frontTh, rearTh)); + var rad = motionTh / 180f * Math.PI; + var dir = new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad)); + if (driveSpeed < 0f) + dir = -dir; + return Vector2.Dot(dir, pathLeft); + } + + private static float ScoreBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft, + float lateral, float biasProbe) + { + ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + biasProbe, baseTh + biasProbe, steerLimit, + out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _); + var lateralVelocity = EstimateLateralVelocity(bodyTh, probeFrontTh, probeRearTh, probeSpeed, pathLeft); + return -Math.Sign(lateral) * lateralVelocity; + } + + private static float GetLateralBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft, + float lateral) + { + if (Math.Abs(lateral) <= 1e-3f) + return 1f; + + const float probeBias = 1f; + var positiveScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, probeBias); + var negativeScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, -probeBias); + return positiveScore >= negativeScore ? 1f : -1f; + } + + private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY, + out float centerTh, out string source) + { + if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh)) + { + source = "fleet"; + return true; + } + + if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) + { + source = "slam"; + return true; + } + + source = "none"; + return false; + } + + public override IEnumerable Get() + { + var self = PilotDefinition.Self; + var conf = PilotDefinition.Conf; + var chassis = BasicPilotBase.Chassis as MultiWheelChassis; + if (chassis == null) + { + DLog.Log("ABORT: FleetCrabWalk requires MultiWheelChassis.", "FleetCrabDbg"); + yield break; + } + _stopping = false; + + DLog.Log( + $"ENTER master?={conf.MultiVehicleMasterEndpoint == "/"} endpoint={conf.MultiVehicleMasterEndpoint} " + + $"fleetNum={conf.MultiVehicleFleetNum} useDetect={conf.MultiVehicleUseDetect} " + + $"syncUseDetour={conf.MultiVehicleSyncUseDetour} useIdealCenter={conf.MultiVehicleAutoUseIdealCenter} " + + $"autoFields=true pathMode=relative pathAngle={CrabAngleDeg:0.0} " + + $"bodyToPath={BodyToPathAngleDeg:0.0} gcpLimit={GcpThetaThreshold:0.0} " + + $"biasFac={conf.BiasFac:0.00} fleetCrabDthFac={conf.FleetCrabDthLinearFac:0.00}", + "FleetCrabDbg"); + + if (conf.MultiVehicleMasterEndpoint != "/") + { + DLog.Log($"ABORT: 非主车 (endpoint={conf.MultiVehicleMasterEndpoint})", "FleetCrabDbg"); + Hedingben.ToastText("车队蟹行需在主车(主车端点=\"/\")运行", "FleetCrab"); + yield break; + } + + // 注意:getCartLocation() 在无有效 Detour 定位时会阻塞——若卡在这里且后面看不到 CENTER 日志,即定位未就绪。 + DLog.Log("主车校验通过,开始读取车队中心 (getCartLocation 无定位会阻塞)…", "FleetCrabDbg"); + if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource)) + { + DLog.Log("ABORT: TryGetFleetCenterFromSlam 返回 false (无定位)", "FleetCrabDbg"); + Hedingben.ToastText("车队蟹行需要主车 Detour 定位", "FleetCrab"); + yield break; + } + DLog.Log($"CENTER 车队中心=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg"); + + DLog.Log($"CENTER_SOURCE source={initialCenterSource} center=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg"); + + var pathStart = new Vector2(x0, y0); + var pathLengthMm = CrabLengthMm; + var phi = CommonMath.RoundTh(theta + CrabAngleDeg); + var dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0)); + var targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg); + var phiRad = phi / 180.0 * Math.PI; + var pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad)); + var pathLeft = new Vector2(-pathDir.Y, pathDir.X); + + DLog.Log( + $"START center=({x0:0},{y0:0},{theta:0.0}) pathMode=relative " + + $"src=({pathStart.X:0},{pathStart.Y:0}) pathAngle={CrabAngleDeg:0.0} bodyToPath={BodyToPathAngleDeg:0.0} " + + $"phi={phi:0.0} targetBody={targetBodyTh:0.0} " + + $"len={pathLengthMm:0} dst=({dst.X:0},{dst.Y:0}) speed={CrabSpeed:0.000} startAccel={FleetCrabStartAccel:0.000} accel={FleetCrabAccel:0.000} " + + $"slow={FleetCrabSlowDistance:0} finishDist={FleetCrabFinishDistance:0} " + + $"finishSpeed={FleetCrabFinishSpeed:0.000} slowingPow={FleetCrabSlowingPow:0.00}", + "FleetCrabDbg"); + + var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold)); + var controlRadius = Math.Max(1f, Math.Abs(conf.TestCarSyncDistance) / 2f); + ResolveCrabDriveEquivalent(0f, (float)CommonMath.ThDiff(phi, theta), + (float)CommonMath.ThDiff(phi, theta), gcpLimit, out _, out var holdFrontTh, out var holdRearTh, + out _, out _); + var warmStart = DateTime.Now; + var warmSeqBaseline = self.BeginFleetMotionWarmup(); + + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = holdFrontTh; + self.MultiVehicleAutoRearTh = holdRearTh; + self.MultiVehicleAutoIdealX = pathStart.X; + self.MultiVehicleAutoIdealY = pathStart.Y; + self.MultiVehicleAutoIdealTh = targetBodyTh; + self.MultiVehicleAutoHasIdeal = true; + self.MultiVehicleAutoCmdTime = DateTime.Now; + self.PrimeMasterAutoFromSlam(); + DLog.Log( + $"WARMUP auto fields enabled, waiting for fleet startup sync seqBase={warmSeqBaseline} " + + $"hold=({holdFrontTh:0.00},{holdRearTh:0.00})", + "FleetCrabDbg"); + + var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, conf.FleetCrabStartSyncTimeoutSec)); + var warmIter = 0; + var warmReady = false; + var warmDetail = ""; + while (!_stopping && DateTime.Now < warmEnd) + { + warmIter++; + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = holdFrontTh; + self.MultiVehicleAutoRearTh = holdRearTh; + self.MultiVehicleAutoIdealX = pathStart.X; + self.MultiVehicleAutoIdealY = pathStart.Y; + self.MultiVehicleAutoIdealTh = targetBodyTh; + self.MultiVehicleAutoHasIdeal = true; + self.MultiVehicleAutoCmdTime = DateTime.Now; + self.PrimeMasterAutoFromSlam(); + var snap = self.GetFleetCenterSnapshot(); + int cnt; + lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count; + if (warmIter % 5 == 0) + DLog.Log( + $"WARMUP#{warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) tick={snap.Tick} " + + $"autoEn={self.MultiVehicleAutoEnabled} scriptEn={self.MultiVehicleScriptEnabled} cnt={cnt}/{conf.MultiVehicleFleetNum} " + + $"detail={warmDetail}", + "FleetCrabDbg"); + if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline, + conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail)) + { + warmReady = true; + DLog.Log( + $"WARMUP done iter={warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) cnt={cnt} detail={warmDetail}", + "FleetCrabDbg"); + break; + } + yield return true; + } + if (!warmReady) + { + DLog.Log($"WARMUP timeout: fleet startup sync failed, abort action. detail={warmDetail}", + "FleetCrabDbg"); + Hedingben.ToastText("车队蟹行启动同步超时,已取消", "FleetCrab"); + Cleanup(); + yield break; + } + + Hedingben.ToastText($"车队蟹行 路径{phi:0.0}° 车身夹角{BodyToPathAngleDeg:0.0}° 长度{pathLengthMm:0}mm", "FleetCrab"); + + if (warmReady && self.TryGetFleetCenterFromMembers(out var warmX, out var warmY, out var warmTh)) + { + x0 = warmX; + y0 = warmY; + theta = warmTh; + pathStart = new Vector2(x0, y0); + phi = CommonMath.RoundTh(theta + CrabAngleDeg); + dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0)); + targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg); + phiRad = phi / 180.0 * Math.PI; + pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad)); + pathLeft = new Vector2(-pathDir.Y, pathDir.X); + self.MultiVehicleAutoIdealX = pathStart.X; + self.MultiVehicleAutoIdealY = pathStart.Y; + self.MultiVehicleAutoIdealTh = targetBodyTh; + self.MultiVehicleAutoCmdTime = DateTime.Now; + DLog.Log( + $"WARMUP_REBASE source=fleet center=({x0:0},{y0:0},{theta:0.0}) phi={phi:0.0} targetBody={targetBodyTh:0.0} dst=({dst.X:0},{dst.Y:0})", + "FleetCrabDbg"); + } + + var iter = 0; + var lastLog = DateTime.MinValue; + var finishDistance = Math.Max(0f, FleetCrabFinishDistance); + var slowDistance = Math.Max(finishDistance + 1f, FleetCrabSlowDistance); + var baseSpeed = Math.Abs(CrabSpeed); + var finishSpeed = Math.Min(baseSpeed, Math.Abs(FleetCrabFinishSpeed)); + var slowingPow = Math.Max(0.01f, FleetCrabSlowingPow); + var accel = Math.Abs(FleetCrabAccel); + var startAccel = Math.Abs(FleetCrabStartAccel); + var cmdSpeed = 0f; + var lastTick = DateTime.Now; + var speedRampStart = DateTime.Now; + var stopReason = "done"; + + while (!_stopping) + { + iter++; + + if (!TryGetControlFleetCenter(self, out var cx, out var cy, out var cth, out var centerSource)) + { + stopReason = "fleet center invalid"; + DLog.Log("ABORT: TryGetControlFleetCenter returned false during auto crab.", "FleetCrabDbg"); + break; + } + var delta = new Vector2(cx - pathStart.X, cy - pathStart.Y); + var along = Vector2.Dot(delta, pathDir); + var lateral = Vector2.Dot(delta, pathLeft); + var remain = pathLengthMm - along; + if (remain <= finishDistance) + break; + + var targetSpeed = baseSpeed; + var slowRatio = 1f; + if (remain < slowDistance) + { + slowRatio = (float)Math.Pow(Clamp(Math.Max(0, remain) / slowDistance, 0f, 1f), slowingPow); + targetSpeed = slowRatio * (baseSpeed - finishSpeed) + finishSpeed; + } + var now = DateTime.Now; + var dt = Math.Max(0.001f, (float)(now - lastTick).TotalSeconds); + lastTick = now; + var rampElapsed = (now - speedRampStart).TotalSeconds; + var activeAccel = rampElapsed < 5.0 ? startAccel : accel; + var speed = activeAccel > 0 ? Slew(cmdSpeed, targetSpeed, activeAccel * dt) : targetSpeed; + cmdSpeed = speed; + + var baseCrabTh = (float)CommonMath.ThDiff(phi, cth); + var headingErr = (float)CommonMath.ThDiff(targetBodyTh, cth); + var headingErrReverse = (float)CommonMath.ThDiff(cth, targetBodyTh); + var targetBodyToPath = (float)CommonMath.ThDiff(phi, targetBodyTh); + var rawBiasMagnitude = (float)(Math.Atan(conf.BiasFac * Math.Abs(lateral) / 1000f / + Math.Max(speed, 0.3f)) / Math.PI * 180.0); + var biasSign = GetLateralBiasSign(baseCrabTh, cth, speed, gcpLimit, pathLeft, lateral); + var rawBiasItem = rawBiasMagnitude * biasSign; + var biasItem = ClampAbs(rawBiasItem, conf.BiasThreshold); + var yawSplitSign = GetYawSplitSign(baseCrabTh + biasItem, speed, gcpLimit, controlRadius); + var rawDthItem = conf.FleetCrabDthLinearFac * headingErr * yawSplitSign; + var dthItem = ClampAbs(rawDthItem, conf.FleetCrabDthLinearThreshold); + var rawFrontTh = baseCrabTh + biasItem + dthItem; + var rawRearTh = baseCrabTh + biasItem - dthItem; + ResolveCrabDriveEquivalent(speed, rawFrontTh, rawRearTh, gcpLimit, out var driveSpeed, + out var frontTh, out var rearTh, out var reverseEquivalent, out var rawBaseTh); + holdFrontTh = frontTh; + holdRearTh = rearTh; + var idealAlong = Clamp(along, 0f, pathLengthMm); + var ideal = pathStart + pathDir * idealAlong; + + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = driveSpeed; + self.MultiVehicleAutoFrontTh = frontTh; + self.MultiVehicleAutoRearTh = rearTh; + self.MultiVehicleAutoIdealX = ideal.X; + self.MultiVehicleAutoIdealY = ideal.Y; + self.MultiVehicleAutoIdealTh = targetBodyTh; + self.MultiVehicleAutoHasIdeal = true; + self.MultiVehicleAutoCmdTime = DateTime.Now; + + if ((DateTime.Now - lastLog).TotalMilliseconds >= 300) + { + lastLog = DateTime.Now; + var snap = self.GetFleetCenterSnapshot(); + int fleetCnt; + lock (self.FleetLock) fleetCnt = self.MultiVehicleFleet.Count; + DLog.Log( + $"ITER#{iter} centerSrc={centerSource} center=({cx:0},{cy:0},{cth:0.0}) snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " + + $"along={along:0} lateral={lateral:0} remain={remain:0} headingErr={headingErr:0.0} " + + $"baseTh={baseCrabTh:0.0} bias={biasItem:0.0} dth={dthItem:0.0} " + + $"slowRatio={slowRatio:0.000} targetV={targetSpeed:0.000} rampT={rampElapsed:0.0} accel={activeAccel:0.000} auto=(vx:{driveSpeed:0.000},fTh:{frontTh:0.0},rTh:{rearTh:0.0}) " + + $"ideal=({ideal.X:0},{ideal.Y:0},{targetBodyTh:0.0}) scriptEn={self.MultiVehicleScriptEnabled} " + + $"cnt={fleetCnt}/{conf.MultiVehicleFleetNum}", + "FleetCrabDbg"); + DLog.Log( + $"CTRL iter={iter} centerSrc:{centerSource} phi:{phi:0.00} targetBody:{targetBodyTh:0.00} startTheta:{theta:0.00} " + + $"cth:{cth:0.00} crabAngle:{CrabAngleDeg:0.00} bodyToPathCfg:{BodyToPathAngleDeg:0.00} " + + $"targetBodyToPath:{targetBodyToPath:0.00} bodyToPathNow:{baseCrabTh:0.00} " + + $"headingErr(target-current):{headingErr:0.00} reverse(current-target):{headingErrReverse:0.00} yawSign:{yawSplitSign:0} " + + $"fleetCrabDthFac:{conf.FleetCrabDthLinearFac:0.000} rawDth:{rawDthItem:0.00} dth:{dthItem:0.00} dthLimit:{conf.FleetCrabDthLinearThreshold:0.00} " + + $"lateral:{lateral:0.0} biasFac:{conf.BiasFac:0.000} biasSign:{biasSign:0} rawBias:{rawBiasItem:0.00} bias:{biasItem:0.00} biasLimit:{conf.BiasThreshold:0.00} " + + $"baseTh:{baseCrabTh:0.00} rawBase:{rawBaseTh:0.00} rawOut(f:{rawFrontTh:0.00},r:{rawRearTh:0.00}) " + + $"out(f:{frontTh:0.00},r:{rearTh:0.00}) gcpLimit:{gcpLimit:0.00} revEq:{reverseEquivalent} " + + $"speedRaw:{speed:0.000} speed:{driveSpeed:0.000} rampT:{rampElapsed:0.0} accel:{activeAccel:0.000} along:{along:0.0} remain:{remain:0.0} ideal=({ideal.X:0.0},{ideal.Y:0.0},{targetBodyTh:0.00})", + "FleetCrabHeadingDbg"); + } + yield return true; + } + + if (_stopping) + stopReason = "stop"; + + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = holdFrontTh; + self.MultiVehicleAutoRearTh = holdRearTh; + self.MultiVehicleAutoCmdTime = DateTime.Now; + DLog.Log( + $"STOP_HOLD iter={iter} reason={stopReason} hold=(fTh:{holdFrontTh:0.0},rTh:{holdRearTh:0.0}) cmdSpeed={cmdSpeed:0.000}", + "FleetCrabDbg"); + var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3)); + while (!_stopping && DateTime.Now < settleEnd) + { + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = holdFrontTh; + self.MultiVehicleAutoRearTh = holdRearTh; + self.MultiVehicleAutoCmdTime = DateTime.Now; + yield return true; + } + + Cleanup(); + Hedingben.ToastText("车队蟹行完成", "FleetCrab"); + DLog.Log($"DONE iter={iter} reason={stopReason}", "FleetCrabDbg"); + } +} diff --git a/.task8-sweep/FleetCurveWalk.cs b/.task8-sweep/FleetCurveWalk.cs new file mode 100644 index 0000000..c5b2e2e --- /dev/null +++ b/.task8-sweep/FleetCurveWalk.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using FundamentalLib; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using MDCSToolBox.Clumsy.MotionControllers; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MDCSToolBox.Clumsy.Tracks; + +namespace MultiWheelC; + +public class FleetCurveWalk : MovementDefinition +{ + public BezierTrack Track; + public List ControlPoints = new(); + public float CurveSpeed = 0.2f; + public float CarDirectionBias = 0f; + public int BezierResolution = 100; + public float SlowDistance = 2000f; + public float FinishDistance = 20f; + public float FinishSpeed = 0.02f; + public float SlowingPow = 0.8f; + public float GcpThetaThreshold = 95f; + public float StartSyncTimeoutSec = 8f; + + private bool _stopping; + private MultiWheelGeometricController _controller; + private MultiWheelChassis _chassis; + private bool _savedControlPoints; + private float _savedControlRadius; + private Vector2 _savedGcp0; + private Vector2 _savedGcp1; + + public void Stop() + { + _stopping = true; + if (_controller != null) + _controller.BreakAndHold = true; + Cleanup(); + } + + public static bool TryParsePointList(string text, out List points, out string error) + { + points = new List(); + error = ""; + if (string.IsNullOrWhiteSpace(text)) + { + error = "empty control point list"; + return false; + } + + var segments = text.Split(new[] { ';', '|' }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < segments.Length; i++) + { + var pair = segments[i].Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (pair.Length != 2) + { + error = $"invalid point #{i + 1}: {segments[i]}"; + return false; + } + + if (!TryParseFloat(pair[0], out var x) || !TryParseFloat(pair[1], out var y)) + { + error = $"invalid number in point #{i + 1}: {segments[i]}"; + return false; + } + points.Add(new Vector2(x, y)); + } + + if (points.Count < 3) + { + error = "Bezier curve requires at least 3 control points"; + return false; + } + + return true; + } + + public static List BuildRelativeControlPoints(Vector2 start, float startTh, List relativePoints) + { + var source = relativePoints ?? new List(); + var normalized = new List(); + if (source.Count == 0 || Vector2.Distance(source[0], Vector2.Zero) > 1f) + normalized.Add(Vector2.Zero); + for (var i = 0; i < source.Count; i++) + normalized.Add(source[i]); + if (normalized.Count < 2) + normalized.Add(new Vector2(1000f, 0f)); + if (normalized.Count < 3) + normalized.Add(new Vector2(2000f, 0f)); + + var result = new List(); + for (var i = 0; i < normalized.Count; i++) + result.Add(CommonMath.Transform2D(start, startTh, normalized[i])); + return result; + } + + public static List BuildAgvControlPoints(float srcX, float srcY, float dstX, float dstY, + params float[] controlPointCoords) + { + var src = new Vector2(srcX, srcY); + var dst = new Vector2(dstX, dstY); + var result = new List(); + if (controlPointCoords == null || controlPointCoords.Length == 0) + { + result.Add(src); + result.Add((src + dst) / 2f); + result.Add(dst); + return result; + } + + if (controlPointCoords.Length % 2 != 0) + throw new ArgumentException("FleetCurve controlPointCoords must contain x,y pairs."); + + var supplied = new List(); + for (var i = 0; i < controlPointCoords.Length; i += 2) + supplied.Add(new Vector2(controlPointCoords[i], controlPointCoords[i + 1])); + + if (supplied.Count >= 3 && + Vector2.Distance(supplied[0], src) <= 10f && + Vector2.Distance(supplied[supplied.Count - 1], dst) <= 10f) + return supplied; + + result.Add(src); + for (var i = 0; i < supplied.Count; i++) + result.Add(supplied[i]); + result.Add(dst); + if (result.Count < 3) + result.Insert(1, (src + dst) / 2f); + return result; + } + + private static bool TryParseFloat(string text, out float value) + { + return float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value) || + float.TryParse(text, out value); + } + + private static float ClampAbs(float value, float limit) + { + var absLimit = Math.Abs(limit); + if (absLimit <= 0) return value; + if (value > absLimit) return absLimit; + if (value < -absLimit) return -absLimit; + return value; + } + + private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY, + out float centerTh, out string source) + { + if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh)) + { + source = "fleet"; + return true; + } + + if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh)) + { + source = "slam"; + return true; + } + + source = "none"; + return false; + } + + private void Cleanup() + { + var self = PilotDefinition.Self; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = 0; + self.MultiVehicleAutoRearTh = 0; + self.MultiVehicleAutoHasIdeal = false; + self.MultiVehicleAutoEnabled = false; + RestoreControlPointRadius(); + } + + private void ApplyFleetControlPointRadius(MultiWheelChassis chassis, float radius) + { + if (!_savedControlPoints) + { + _chassis = chassis; + _savedControlRadius = chassis.ControlPointRadius; + var gcps = chassis.GetGeometricControlPoints(); + if (gcps.Count >= 2) + { + _savedGcp0 = gcps[0].Position; + _savedGcp1 = gcps[1].Position; + } + _savedControlPoints = true; + } + + chassis.ControlPointRadius = radius; + var points = chassis.GetGeometricControlPoints(); + if (points.Count >= 2) + { + points[0].Position = new Vector2(radius, 0); + points[1].Position = new Vector2(-radius, 0); + } + } + + private void RestoreControlPointRadius() + { + if (!_savedControlPoints || _chassis == null) + return; + + _chassis.ControlPointRadius = _savedControlRadius; + var points = _chassis.GetGeometricControlPoints(); + if (points.Count >= 2) + { + points[0].Position = _savedGcp0; + points[1].Position = _savedGcp1; + } + _savedControlPoints = false; + } + + private static void WriteWarmupAuto(PilotDefinition self, Vector2 idealPos, float idealTh, + float frontTh, float rearTh) + { + self.MultiVehicleScriptEnabled = false; + self.MultiVehicleScriptMode = 0; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoFrontTh = frontTh; + self.MultiVehicleAutoRearTh = rearTh; + self.MultiVehicleAutoIdealX = idealPos.X; + self.MultiVehicleAutoIdealY = idealPos.Y; + self.MultiVehicleAutoIdealTh = idealTh; + self.MultiVehicleAutoHasIdeal = true; + self.MultiVehicleAutoCmdTime = DateTime.Now; + } + + public override IEnumerable Get() + { + var self = PilotDefinition.Self; + var conf = PilotDefinition.Conf; + var chassis = BasicPilotBase.Chassis as MultiWheelChassis; + _stopping = false; + + if (chassis == null) + { + DLog.Log("ABORT: FleetCurveWalk requires MultiWheelChassis.", "FleetCurveDbg"); + yield break; + } + + if (conf.MultiVehicleMasterEndpoint != "/") + { + DLog.Log($"ABORT: FleetCurveWalk must run on master endpoint, endpoint={conf.MultiVehicleMasterEndpoint}", + "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve requires master vehicle", "FleetCurve"); + yield break; + } + + if (Track == null && (ControlPoints == null || ControlPoints.Count < 3)) + { + DLog.Log("ABORT: FleetCurveWalk requires a BezierTrack or at least 3 control points.", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve requires track or >=3 control points", "FleetCurve"); + yield break; + } + + if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource)) + { + DLog.Log("ABORT: FleetCurveWalk failed to read fleet center.", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve"); + yield break; + } + + var baseSpeed = Math.Abs(CurveSpeed); + if (baseSpeed <= 1e-4f) + { + DLog.Log("ABORT: FleetCurveWalk speed is zero.", "FleetCurveDbg"); + yield break; + } + + var resolution = Math.Max(2, BezierResolution); + var speedFinish = Math.Min(baseSpeed, Math.Abs(FinishSpeed)); + var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold)); + var controlRadius = Math.Max(1f, Math.Abs(conf.TestCarSyncDistance) / 2f); + + ApplyFleetControlPointRadius(chassis, controlRadius); + + try + { + var track = Track; + var trackSource = "external"; + if (track == null) + { + var points = new List(ControlPoints); + track = new BezierTrack(points, resolution); + trackSource = "controlPoints"; + } + track.CarDirectionBias = CarDirectionBias; + track.Speed = baseSpeed; + + var center = new Vector2(x0, y0); + var (idealPos, idealAngle, bias, pd) = track.QueryTangentPoint(center); + var carDirection = (float)CommonMath.ThDiff(theta, CarDirectionBias); + var holdTh = ClampAbs((float)CommonMath.ThDiff(idealAngle, carDirection), gcpLimit); + var targetBodyTh = (float)CommonMath.RoundTh(idealAngle + CarDirectionBias); + + DLog.Log( + $"START center=({x0:0},{y0:0},{theta:0.0}) source={initialCenterSource} " + + $"track={track.GetType().Name} trackSource={trackSource} controls={ControlPoints?.Count ?? 0} " + + $"len={track.Length():0} speed={baseSpeed:0.000} bias={CarDirectionBias:0.0} " + + $"query=({idealPos.X:0},{idealPos.Y:0}) tangent={idealAngle:0.0} targetBody={targetBodyTh:0.0} " + + $"pathBias={bias:0.0} pd={pd:0.0} hold={holdTh:0.0} radius={controlRadius:0}", + "FleetCurveDbg"); + + var warmStart = DateTime.Now; + var warmSeqBaseline = self.BeginFleetMotionWarmup(); + WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh); + self.PrimeMasterAutoFromSlam(); + + var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, StartSyncTimeoutSec)); + var warmIter = 0; + var warmReady = false; + var warmDetail = ""; + while (!_stopping && DateTime.Now < warmEnd) + { + warmIter++; + WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh); + self.PrimeMasterAutoFromSlam(); + + if (warmIter % 5 == 0) + { + var snap = self.GetFleetCenterSnapshot(); + int cnt; + lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count; + DLog.Log( + $"WARMUP#{warmIter} snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " + + $"cnt={cnt}/{conf.MultiVehicleFleetNum} detail={warmDetail}", + "FleetCurveDbg"); + } + + if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline, + conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail)) + { + warmReady = true; + DLog.Log($"WARMUP done iter={warmIter} detail={warmDetail}", "FleetCurveDbg"); + break; + } + yield return true; + } + + if (!warmReady) + { + DLog.Log($"WARMUP timeout: fleet startup sync failed, abort curve action. detail={warmDetail}", + "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve startup sync timeout", "FleetCurve"); + Cleanup(); + yield break; + } + + _controller = new ChassisController { BaseSpeed = baseSpeed }.Get(); + _controller.MultiVehicleSync = true; + _controller.BaseSpeed = baseSpeed; + _controller.SlowDistance = Math.Max(FinishDistance + 1f, SlowDistance); + _controller.FinishDistance = Math.Max(0f, FinishDistance); + _controller.FinishSpeed = speedFinish; + _controller.SlowingPow = Math.Max(0.01f, SlowingPow); + _controller.GcpThetaThreshold = gcpLimit; + _controller.AddTrack(track, "FleetCurve"); + + Hedingben.ToastText($"FleetCurve len {track.Length():0}mm speed {baseSpeed:0.00}", "FleetCurve"); + + foreach (var running in _controller.Track()) + { + if (_stopping) + break; + if (!running) + break; + yield return true; + } + + if (!_stopping) + { + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoCmdTime = DateTime.Now; + var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3)); + while (!_stopping && DateTime.Now < settleEnd) + { + self.MultiVehicleAutoEnabled = true; + self.MultiVehicleAutoVx = 0; + self.MultiVehicleAutoCmdTime = DateTime.Now; + yield return true; + } + } + + DLog.Log($"DONE stopping={_stopping}", "FleetCurveDbg"); + } + finally + { + Cleanup(); + _controller = null; + } + } +} diff --git a/.task8-sweep/MovementTests.TireFollowing.cs b/.task8-sweep/MovementTests.TireFollowing.cs new file mode 100644 index 0000000..ff3598e --- /dev/null +++ b/.task8-sweep/MovementTests.TireFollowing.cs @@ -0,0 +1,606 @@ +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using ClumsyCore.Utilities; +using ClumsyDance.ClumsyDance.Detectors; +using ClumsyDance.ClumsyWalk.Detectors; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using FundamentalLib; +using MDCSToolBox.Clumsy.Calibration; +using MDCSToolBox.Clumsy.Tracks; +using MDCSToolBox.Commons.Controllers; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Threading; +using LineSegment = ClumsyCore.Utilities.LineSegment; + +namespace MultiWheelC +{ + [MovementTest(name = "轮胎检测")] + public class TireDetect : MovementTest + { + public override void TestStop() + { + _running = false; + } + + public override void Test() + { + _painter = UI.GetPainter("TwoLegDetectTest", false); + _painter.Clear(); + + var frontlidar = UI.GetInput("1是用前雷达识别,2是用后雷达识别"); + var result = int.Parse(frontlidar.ToString()); + var lastDetectX = result == 1 ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX; + var lastDetectY = 0f; + while (_running) + { + var ld = Detect(lastDetectX, SetFilters(lastDetectX, lastDetectY), result == 1 ? true : false); + if(ld == null) + { + //Console.WriteLine("ld == null"); + continue; + } + _painter.Clear(); + var center = (ld.Src + ld.Dst) / 2; + var distanceToCarOrigin = Vector2.Distance(Vector2.Zero, center); + var distanceLabelPos = center / 2; + _painter.DrawLine(Color.Cyan, Vector2.Zero, center, width: 2); + _painter.DrawText(Color.Yellow, $"{distanceToCarOrigin:F3}", distanceLabelPos.X, distanceLabelPos.Y); + lastDetectX = center.X; + lastDetectY = center.Y; + Thread.Sleep(100); + } + } + + public static LineSegment Detect(float guessX, List filters, bool frontlidar) + { + return new Lidar2dDetect2LegTray() + { + BlobDist = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegBlobDist : PilotDefinition.Conf.TireBackTwoLegBlobDist, + BlobPtCount = frontlidar ? PilotDefinition.Conf.TireTwoLegBlobPtCount : PilotDefinition.Conf.TireTwoLegBlobPtCount, + BlobSize = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegBlobSize : PilotDefinition.Conf.TireBackTwoLegBlobSize, + CenterChange = Tuple.Create(frontlidar ? PilotDefinition.Conf.TireFrontTwoLegCenterChangeX : PilotDefinition.Conf.TireBackTwoLegCenterChangeX, 0f, 0f), + LegWidth = PilotDefinition.Conf.TireTwoLegWidth, + LegWidthErr = frontlidar ? PilotDefinition.Conf.TireTwoLegWidthErr : PilotDefinition.Conf.TireTwoLegWidthErr, + Padding = frontlidar ? PilotDefinition.Conf.TireFrontPadding : PilotDefinition.Conf.TireBackPadding, + PillarFindingScope = frontlidar ? PilotDefinition.Conf.TireFrontTwoLegPillarFindingScope : PilotDefinition.Conf.TireBackTwoLegPillarFindingScope, + SgnDir = PilotDefinition.Conf.TwoLegSgnDir, + }.DetectWithGuess(frontlidar ? "frontlidar" : "leftlidar,rightlidar", new LineSegment(new Vector2(guessX, 0), Vector2.Zero), + guessCoordinateSystem: CoordinateSystem.Car2D, outCoordinateSystem: CoordinateSystem.Car2D, filters); + } + + private List SetFilters(float guessCenterX, float guessCenterY) + { + var painter = UI.GetPainter("GeneralFollowing.SetFilters", false); + painter.Clear(); + painter.Clear(3000); + + var box = new Vector2[] + { + new (guessCenterX - PilotDefinition.Conf.TireFilterLength / 2, guessCenterY - PilotDefinition.Conf.TireFilterWidth / 2), + new (guessCenterX + PilotDefinition.Conf.TireFilterLength / 2, guessCenterY - PilotDefinition.Conf.TireFilterWidth / 2), + new (guessCenterX + PilotDefinition.Conf.TireFilterLength / 2, guessCenterY + PilotDefinition.Conf.TireFilterWidth / 2), + new (guessCenterX - PilotDefinition.Conf.TireFilterLength / 2, guessCenterY + PilotDefinition.Conf.TireFilterWidth / 2), + }; + + for (var i = 0; i < box.Length; ++i) + painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); + + // PC filter in car coordinate frame + return new List() + { + new(CoordinateSystem.Car2D, + p => LessMath.IsPointInPolygon4( + box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), + }; + } + + private Painter _painter; + private bool _running = true; + } + + [MovementTest(name = "钻车测试")] + public class FollowTire : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + } + + public override void Test() + { + var front = UI.GetInput("1是用前雷达识别,2是用后雷达识别"); + var result = int.Parse(front.ToString()); + var lidarname = result == 1 ? "前雷达" : "后雷达"; + DLog.Log($"开始钻车测试,用{lidarname}识别", "TireFollowing"); + + var following = new TireFollowing() + { + GetController = () => new ChassisController().Get(), + GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, + GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, + detectors = new List() + { + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, result == 1 ? true : false), + StartGuessingX = result == 1 ? PilotDefinition.Conf.TireFollowingStage1GuessX : -PilotDefinition.Conf.TireFollowingStage1GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, + result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0) + }, + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, result == 1 ? true : false), + StartGuessingX = result == 1 ? PilotDefinition.Conf.TireFollowingStage2GuessX : -PilotDefinition.Conf.TireFollowingStage2GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationX : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationX, + result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarPathTransformationY : PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0) + }, + }, + CarDirection = result == 1 ? 0f : 180f, + SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, + MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, + TireNum = PilotDefinition.Conf.TireFollowingTireNum, + WalkBlindTh = result == 1 ? PilotDefinition.Conf.TireFollowingFrontLidarWalkBlindTh : PilotDefinition.Conf.TireFollowingBackLidarWalkBlindTh, + }; + _dt = new DriveTask(following.Get()); + _dt.Wait(); + DLog.Log($"结束钻车测试", "TireFollowing"); + } + private DriveTask _dt; + } + + [MovementTest(name = "离车测试")] + public class LeaveCar : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + } + + public override void Test() + { + DLog.Log($"开始离车测试,用后雷达识别", "TireFollowing"); + var following = new TireFollowing() + { + GetController = () => new ChassisController().Get(), + GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, + GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, + detectors = new List() + { + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), + StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingLeaveCarWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, + PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0) + }, + }, + CarDirection = 180f, + SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, + MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, + WalkBlindTh = 0, + TireNum = 1 + }; + _dt = new DriveTask(following.Get()); + _dt.Wait(); + DLog.Log($"结束离车测试", "TireFollowing"); + } + private DriveTask _dt; + } + + [MovementTest(name = "抱夹关闭")] + public class ClampTest1 : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + PilotDefinition.Self.SpeedLeftArm = 0; + PilotDefinition.Self.SpeedRightArm = 0; + } + + public override void Test() + { + _dt = new DriveTask(new ClampToTarget() + { + LeftClampTarget = PilotDefinition.Self.LeftArmUpperPos, + RightClampTarget = PilotDefinition.Self.RightArmUpperPos + }.Get()); + _dt.Wait(); + } + + private DriveTask _dt; + } + + [MovementTest(name = "抱夹打开")] + public class ClampTest2 : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + PilotDefinition.Self.SpeedLeftArm = 0; + PilotDefinition.Self.SpeedRightArm = 0; + } + + public override void Test() + { + _dt = new DriveTask(new ClampToTarget() + { + LeftClampTarget = PilotDefinition.Self.LeftArmLowerPos, + RightClampTarget = PilotDefinition.Self.RightArmLowerPos + }.Get()); + _dt.Wait(); + } + + private DriveTask _dt; + } + + [MovementTest(name = "测试前进基于轮里程")] + public class LineTrackingTest : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + } + public override void Test() + { + _dt = new DriveTask(new LineTracking() + { + Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, + }.Get()); + _dt.Wait(); + } + private DriveTask _dt; + } + + [MovementTest(name = "测试后退基于轮里程")] + public class ReverseLineTrackingTest : MovementTest + { + public override void TestStop() + { + _dt?.Stop(); + } + public override void Test() + { + _dt = new DriveTask(new LineTracking() + { + Target = -PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, + }.Get()); + _dt.Wait(); + } + private DriveTask _dt; + } + + [MovementTest(name = "测试终点跟踪动作-前进")] + public class DstTrackerForward : MovementTest + { + public bool UseInteractivePick = true; + public float srcX; + public float srcY; + public float dstX; + public float dstY; + public float carDirectionBias = 0f; + private readonly Painter _painter = UI.GetPainter("DstTrackerTest"); + + public override void TestStop() + { + _dt?.Stop(); + _painter?.Clear(); + } + public override void Test() + { + var p1 = UI.GetPoint("point1"); + var p2 = UI.GetPoint("point2"); + + _painter.Clear(); + _dt = new DriveTask(new DstTracker() + { + Src = p1, + Dst = p2, + CarDirectionBias = carDirectionBias, + }.Get()); + _dt.Wait(); + } + + private DriveTask _dt; + } + + [MovementTest(name = "测试终点跟踪动作-后退")] + public class DstTrackerhoutui : MovementTest + { + public bool UseInteractivePick = true; + public float srcX; + public float srcY; + public float dstX; + public float dstY; + public float carDirectionBias = 180f; + private readonly Painter _painter = UI.GetPainter("DstTrackerTest"); + + public override void TestStop() + { + _dt?.Stop(); + _painter?.Clear(); + } + public override void Test() + { + var p1 = UI.GetPoint("point1"); + var p2 = UI.GetPoint("point2"); + + _painter.Clear(); + _dt = new DriveTask(new DstTracker() + { + Src = p1, + Dst = p2, + CarDirectionBias = carDirectionBias, + }.Get()); + _dt.Wait(); + } + + private DriveTask _dt; + } + + [MovementTest(name = "测试先直行再终点跟踪")] + public class LineTrackThenDstTrackerTest : MovementTest + { + public float carDirectionBias = 0f; + private readonly Painter _painter = UI.GetPainter("LineTrackThenDstTrackerTest"); + public override void TestStop() + { + _dt?.Stop(); + _painter?.Clear(); + } + public override void Test() + { + var src = UI.GetPoint("请在上位机选择起点(src)"); + var dst = UI.GetPoint("请在上位机选择终点(dst)"); + _painter.Clear(); + _painter.DrawLine(Color.Cyan, src.X, src.Y, dst.X, dst.Y, width: 3); + _painter.DrawCircle(Color.LimeGreen, src.X, src.Y, 80f); + _painter.DrawCircle(Color.OrangeRed, dst.X, dst.Y, 80f); + _painter.DrawText(Color.LimeGreen, "src", src.X + 80f, src.Y + 80f); + _painter.DrawText(Color.OrangeRed, "dst", dst.X + 80f, dst.Y + 80f); + IEnumerable TrackThenFollow() + { + foreach (var running in new LineTracking() + { + Target = PilotDefinition.Conf.LineTrackDistance + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, + EnableHandover = true, + HandoverDistance = 200f, + HandoverSpeed = 0.3f, + }.Get()) + { + if (!running) break; + yield return true; + } + foreach (var running in new DstTracker() + { + Src = src, + Dst = dst, + CarDirectionBias = carDirectionBias, + InitialSendSpeed = 0.3f + }.Get()) + { + if (!running) break; + yield return true; + } + yield return false; + } + _dt = new DriveTask(TrackThenFollow()); + _dt.Wait(); + } + private DriveTask _dt; + } + + [MovementTest(name = "测试先离车再终点跟踪")] + public class LeaveCarThenDstTrackerTest : MovementTest + { + private readonly Painter _painter = UI.GetPainter("LeaveCarThenDstTrackerTest"); + + public override void TestStop() + { + _dt?.Stop(); + _painter?.Clear(); + } + + public override void Test() + { + var src = UI.GetPoint("请在上位机选择离车后起点(src)"); + var dst = UI.GetPoint("请在上位机选择终点(dst)"); + + _painter.Clear(); + _painter.DrawLine(Color.Cyan, src.X, src.Y, dst.X, dst.Y, width: 3); + _painter.DrawCircle(Color.LimeGreen, src.X, src.Y, 80f); + _painter.DrawCircle(Color.OrangeRed, dst.X, dst.Y, 80f); + _painter.DrawText(Color.LimeGreen, "src", src.X + 80f, src.Y + 80f); + _painter.DrawText(Color.OrangeRed, "dst", dst.X + 80f, dst.Y + 80f); + + IEnumerable LeaveThenFollow() + { + var following = new TireFollowing() + { + GetController = () => new ChassisController().Get(), + GuessRangeX = PilotDefinition.Conf.TireFilterLength / 2, + GuessRangeY = PilotDefinition.Conf.TireFilterWidth / 2, + detectors = new List() + { + new TireFollowing.DetectorDefinition() + { + DetectFunction = (_, lastDetectX, filters) => TireDetect.Detect(lastDetectX, filters, false), + StartGuessingX = -PilotDefinition.Conf.TireFollowingStage2GuessX, + StartGuessingY = 0, + SwitchWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindSwitchingDistance, + FinishWalkBlindCondition = rd => rd <= PilotDefinition.Conf.TireFollowingWalkBlindFinishDistance, + PathTransformation = new Tuple( + PilotDefinition.Conf.TireFollowingLeaveCarBackLidarPathTransformationX, + PilotDefinition.Conf.TireFollowingBackLidarPathTransformationY, + 0), + }, + }, + CarDirection = 180f, + SlowDistance = PilotDefinition.Conf.TireFollowingSlowDistance, + MaxSpeed = PilotDefinition.Conf.TireFollowingMaxSpeed, + WalkBlindTh = 0, + TireNum = 1 + }; + + foreach (var running in following.Get()) + { + if (!running) break; + yield return true; + } + + foreach (var running in new DstTracker() + { + Src = src, + Dst = dst, + CarDirectionBias = 180f, + }.Get()) + { + if (!running) break; + yield return true; + } + + yield return false; + } + + _dt = new DriveTask(LeaveThenFollow()); + _dt.Wait(); + } + + private DriveTask _dt; + } + + + [MovementTest(name = "驱动器下使能测试")] + public class DriverDisableTest : MovementTest + { + public override void TestStop() + { + throw new NotImplementedException(); + } + + public override void Test() + { + new DriveTask(new DriverDisable(){ }.Get()).Wait(); + } + } + + [MovementTest(name = "驱动器复位测试")] + public class DriverAbleTest : MovementTest + { + public override void TestStop() + { + throw new NotImplementedException(); + } + + public override void Test() + { + new DriveTask(new DriverAble(){ }.Get()).Wait(); + } + } + + [MovementTest(name = "底盘旋转测试")] + public class RotateToAngleTest : MovementTest + { + public override void TestStop() + { + throw new NotImplementedException(); + } + + public override void Test() + { + var target = UI.GetInput("输入旋转角度:"); + var chassis = (MultiWheelChassis)PilotDefinition.Chassis; + new DriveTask(new MultiWheelRotateInPlace() + { + AngleTarget = float.Parse(target), + PidparamsRead = () => new PIDParams() + { + Kp = PilotDefinition.Conf.TireFollowingThkp, + Ki = PilotDefinition.Conf.TireFollowingThki, + Kd = PilotDefinition.Conf.TireFollowingThkd, + DeadZone = PilotDefinition.Conf.TireFollowingThDeadZone, + SpeedAccPerSec = PilotDefinition.Conf.TireFollowingThSpeedAccPerSec, + OutputUpperThreshold = PilotDefinition.Conf.TireFollowingThThresh, + MaxI = PilotDefinition.Conf.TireFollowingThMaxI, + } + }.Get()).Wait(); + } + } + + public class utils + { + public static List<(float x, float y, float th)> RemoveOutliers(List<(float x, float y, float th)> data, float threshold = 2.0f) + { + var means = CalculateMean(data); + var stdDevs = CalculateStandardDeviation(data, means); + + return data.Where(point => + Math.Abs(point.x - means.x) <= threshold * stdDevs.x && + Math.Abs(point.y - means.y) <= threshold * stdDevs.y && + AngularDistance(point.th, means.th) <= threshold * stdDevs.th + ).ToList(); + } + + public static (float x, float y, float th) CalculateMean(List<(float x, float y, float th)> data) + { + float meanX = data.Average(point => point.x); + float meanY = data.Average(point => point.y); + float sinSum = data.Sum(point => (float)Math.Sin(DegreeToRadian(point.th))); + float cosSum = data.Sum(point => (float)Math.Cos(DegreeToRadian(point.th))); + float meanTh = RadianToDegree((float)Math.Atan2(sinSum, cosSum)); + + return (meanX, meanY, meanTh); + } + + public static (float x, float y, float th) CalculateStandardDeviation(List<(float x, float y, float th)> data, (float x, float y, float th) means) + { + float varianceX = data.Average(point => (point.x - means.x) * (point.x - means.x)); + float varianceY = data.Average(point => (point.y - means.y) * (point.y - means.y)); + + // 计算角度的方差 + float varianceTh = data.Average(point => AngularDistance(point.th, means.th) * AngularDistance(point.th, means.th)); + + return ((float)Math.Sqrt(varianceX), (float)Math.Sqrt(varianceY), (float)Math.Sqrt(varianceTh)); + } + + public static float DegreeToRadian(float degree) + { + return (float)(degree * Math.PI / 180.0); + } + + public static float RadianToDegree(float radian) + { + return (float)(radian * 180.0 / Math.PI); + } + + public static float AngularDistance(float angle1, float angle2) + { + return CommonMath.ThDiff(angle1, angle2); + } + } + +} + + diff --git a/.task8-sweep/MovementTests.TwoLegDetect.cs b/.task8-sweep/MovementTests.TwoLegDetect.cs new file mode 100644 index 0000000..c0a4f57 --- /dev/null +++ b/.task8-sweep/MovementTests.TwoLegDetect.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using System.Threading; +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Pilot; +using ClumsyCore.Utilities; +using ClumsyDance.ClumsyDance.Detectors; +using ClumsyDance.ClumsyWalk.Detectors; +using FundamentalLib; +using LineSegment = ClumsyCore.Utilities.LineSegment; + +namespace MultiWheelC; + +/// +/// 2腿检测:用单线激光雷达识别两腿托盘/轮胎,按上一帧结果作为下一帧猜测做闭环检测。 +/// 从 StandardMultiWheelLifter 移植;参数全部走 PilotConfig(Fields 面板),雷达选择改为配置项而非阻塞输入。 +/// +[MovementTest(name = "多舵轮-2腿检测")] +public class TwoLegDetect : MovementTest +{ + private Painter _painter; + private bool _running = true; + + public override void TestStop() => _running = false; + + public override void Test() + { + _running = true; + _painter = UI.GetPainter("MultiWheelTwoLegDetect", false); + _painter.Clear(); + + var lidar = PilotDefinition.Conf.TwoLegLidarName; + var lastDetectX = PilotDefinition.Conf.TwoLegGuessX; + var lastDetectY = 0f; + + while (_running) + { + var ld = Detect(lidar, lastDetectX, SetFilters(lastDetectX, lastDetectY)); + if (ld == null) + { + Thread.Sleep(100); + continue; + } + + var center = (ld.Src + ld.Dst) / 2; + var distanceToCarOrigin = Vector2.Distance(Vector2.Zero, center); + + _painter.Clear(); + _painter.DrawLine(Color.Cyan, Vector2.Zero, center, width: 2); + _painter.DrawText(Color.Yellow, $"{distanceToCarOrigin:F1}", center.X / 2, center.Y / 2); + + Hedingben.ToastText( + $"[Test检测] lidar:{lidar} guess x:{lastDetectX:F0} y:{lastDetectY:F0} | " + + $"中心 x:{center.X:F0} y:{center.Y:F0} dist:{distanceToCarOrigin:F0}", + "MultiWheelTwoLegDetect-test"); + + // 用本帧中心作为下一帧猜测,实现闭环跟踪 + lastDetectX = center.X; + lastDetectY = center.Y; + Thread.Sleep(100); + } + + _painter.Clear(); + } + + /// 在车体坐标系下,按猜测位置检测两腿,返回连接两腿的线段(车体系)。 + public static LineSegment Detect(string lidarName, float guessX, List filters) + { + var conf = PilotDefinition.Conf; +#pragma warning disable CS0612, CS0618 + var detector = new Lidar2dDetect2LegTray + { + BlobDist = conf.TwoLegBlobDist, + BlobPtCount = conf.TwoLegBlobPtCount, + BlobSize = conf.TwoLegBlobSize, + CenterChange = Tuple.Create(conf.TwoLegCenterChangeX, 0f, 0f), + LegWidth = conf.TwoLegWidth, + LegWidthErr = conf.TwoLegWidthErr, + Padding = conf.TwoLegPadding, + PillarFindingScope = conf.TwoLegPillarFindingScope, + SgnDir = conf.TwoLegSgnDir, + }; +#pragma warning restore CS0612, CS0618 + + var result = detector.DetectWithGuess( + lidarName, + new LineSegment(new Vector2(guessX, 0), Vector2.Zero), + guessCoordinateSystem: CoordinateSystem.Car2D, + outCoordinateSystem: CoordinateSystem.Car2D, + filters); + return ApplyOutputBias(result); + } + + private static LineSegment ApplyOutputBias(LineSegment result) + { + if (result == null) return null; + + var conf = PilotDefinition.Conf; + if (Math.Abs(conf.TwoLegOutputBiasX) < 1e-6f && Math.Abs(conf.TwoLegOutputBiasY) < 1e-6f) + return result; + + var bias = new Vector2(conf.TwoLegOutputBiasX, conf.TwoLegOutputBiasY); + return new LineSegment(result.Src + bias, result.Dst + bias); + } + + /// 在猜测中心周围构造一个矩形 ROI,过滤掉框外点云,降低误识别。 + public static List SetFilters(float guessCenterX, float guessCenterY) + { + var conf = PilotDefinition.Conf; + var painter = UI.GetPainter("MultiWheelTwoLegDetect.Filter", false); + painter.Clear(); + + var box = new[] + { + new Vector2(guessCenterX - conf.TwoLegFilterLength / 2, guessCenterY - conf.TwoLegFilterWidth / 2), + new Vector2(guessCenterX + conf.TwoLegFilterLength / 2, guessCenterY - conf.TwoLegFilterWidth / 2), + new Vector2(guessCenterX + conf.TwoLegFilterLength / 2, guessCenterY + conf.TwoLegFilterWidth / 2), + new Vector2(guessCenterX - conf.TwoLegFilterLength / 2, guessCenterY + conf.TwoLegFilterWidth / 2), + }; + + for (var i = 0; i < box.Length; ++i) + painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); + + // 点云滤波在车体坐标系下进行 + return new List + { + new(CoordinateSystem.Car2D, + p => LessMath.IsPointInPolygon4( + box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), + }; + } +} diff --git a/.task8-sweep/MovementTests.cs b/.task8-sweep/MovementTests.cs new file mode 100644 index 0000000..3d6322d --- /dev/null +++ b/.task8-sweep/MovementTests.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using FundamentalLib; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MDCSToolBox.Clumsy.Tracks; + +namespace MultiWheelC; + +public class MultiForwardTest : MovementDefinition +{ + public float Speed = 0.2f; + public float DurationSeconds = 2f; + + public override IEnumerable Get() + { + var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; + chassis.SetOriginBias(0, 0, 0); + var end = DateTime.Now.AddSeconds(DurationSeconds); + while (DateTime.Now < end) + { + chassis.SendMotion(Speed, 0, 0); + yield return true; + } + + chassis.SendMotion(0, 0, 0); + } +} + +// 原地旋转到指定世界坐标系朝向:先把舵轮打到旋转所需角度,对齐后再旋转,按目标角度停止(非固定时长)。 +public class MultiRotateToWorldAngle : MovementDefinition +{ + /// 目标朝向(世界坐标系,单位 deg)。 + public float TargetWorldDeg; + + /// 旋转角速度(deg/s,逆时针为正)。 + public float RotSpeed = 30f; + + /// 到位角度精度(deg)。 + public float ArriveDeg = 1f; + + /// 起转前舵轮对齐精度(deg)。 + public float WheelAlignDeg = 2f; + + public override IEnumerable Get() + { + var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; + chassis.SetOriginBias(0, 0, 0); + + // 阶段一:仅把舵轮打到原地旋转所需角度(下发 0 速度,只对齐不旋转)。 + while (true) + { + chassis.SendRotateMotion(0); + if (WheelsAligned(chassis, WheelAlignDeg)) break; + yield return true; + } + + // 阶段二:旋转到目标世界朝向,到位即停。 + var target = CommonMath.RoundTh(TargetWorldDeg); + while (true) + { + var cur = CommonMath.RoundTh((float)DetourInterface.getCartLocation().th); + var diff = CommonMath.ThDiff(target, cur); // 逆时针为正 + if (Math.Abs(diff) <= ArriveDeg) break; + chassis.SendRotateMotion(Math.Sign(diff) * RotSpeed); + yield return true; + } + + chassis.PredefinedDriveStop(); + } + + private static bool WheelsAligned(MultiWheelChassis chassis, float tolDeg) + { +#pragma warning disable CS0612, CS0618 + var wheels = chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + foreach (var sw in wheels) + if (Math.Abs(CommonMath.ThDiff(sw.ReadAngle(), sw.GetSendAngle())) > tolDeg) + return false; + return true; + } +} + +[MovementTest(name = "多舵轮-前进2秒")] +public class MultiForwardMovementTest : MovementTest +{ + private DriveTask _task; + + public override void Test() + { + _task = new DriveTask(new MultiForwardTest().Get()); + _task.Wait(); + } + + public override void TestStop() => _task?.Stop(); +} + +[MovementTest(name = "多舵轮-原地旋转到目标角度")] +public class MultiRotateMovementTest : MovementTest +{ + private DriveTask _task; + + public override void Test() + { + _task = new DriveTask(new MultiRotateToWorldAngle + { + TargetWorldDeg = PilotDefinition.Conf.InPlaceRotateTargetWorldDeg, + RotSpeed = PilotDefinition.Conf.InPlaceRotateSpeed, + ArriveDeg = PilotDefinition.Conf.InPlaceRotateArriveDeg, + WheelAlignDeg = PilotDefinition.Conf.InPlaceRotateWheelAlignDeg + }.Get()); + _task.Wait(); + } + + public override void TestStop() + { + _task?.Stop(); + ((MultiWheelChassis)BasicPilotBase.Chassis).PredefinedDriveStop(); + } +} + +// ===== 车队联动-原地旋转动作 ===== +// 等价于 FleetRemote 的「原地旋转」模式(已实测可用):FleetRemote 通过 Medulla 手动 IO +// (MultiVehicleManualEnabled + Mode=2 + Vth) 驱动 PilotDefinition.TickMultiVehicle 绕车队中心旋转。 +// 手动 IO 是 [AsLowerIO](Medulla→Clumsy,每周期回写),Clumsy 侧动作直接写会被覆盖; +// 因此本动作改用 Clumsy 内部脚本字段 MultiVehicleScript*(TickMultiVehicle 已将其作为手动等价输入), +// 不写一行底盘指令——实际的 SendRotateMotion + PI 纠偏 + 向从车广播均由 TickMultiVehicle 完成。 +// +// 前提:在「主车」(MultiVehicleMasterEndpoint == "/") 的 Clumsy 上运行,且从车已注册(编队就绪)。 +// 停止条件:主车 SLAM 朝向累计转过 |TargetDeltaDeg|(刚体原地旋转,整车朝向变化量 == 车队转角); +// 无定位时退化为按 |TargetDeltaDeg| / |Omega| 估算时长;并带安全超时。 +public class FleetRotateInPlace : MovementDefinition +{ + /// 角速度大小(deg/s);实际方向由 TargetDeltaDeg 的符号决定。 + public float Omega = 15f; + + /// 目标相对转角(deg,带符号,+ 为逆时针)。 + public float TargetDeltaDeg = 90f; + + /// 到位角度精度(deg)。 + public float ArriveDeg = 1.5f; + + /// 减速区宽度(deg):剩余角度小于此值时,角速度按剩余比例线性降到 MinOmega,抑制惯性超调。 + public float SlowDeg = 25f; + + /// 减速区末段最小角速度(deg/s):避免越接近目标越慢、长尾停不下/到不了位。 + public float MinOmega = 3f; + + /// 缓启动角加速度(deg/s²):起步时角速度从 0 按此斜率爬升到巡航值,抑制起步抖动/队形骤偏。仅作用于起步加速,<=0 关闭缓启动(阶跃起步)。 + public float AccelDegPerSec2 = 20f; + + /// + /// 是否用 Detour 主车航向闭环判停(读 getCartLocation().th 累计实际转角,到 |TargetDeltaDeg| 停)。 + /// 与 MultiVehicleSyncUseDetour 解耦:转到指定角度需要角度反馈,故默认 true。 + /// false 时退化为按估算时长开环停止(实际转速≠指令时不精确)。注意 true 时若无有效全局定位, + /// getCartLocation() 会阻塞(与单车 MultiRotateToWorldAngle 行为一致)。 + /// + public bool UseDetourHeading = true; + + // 注:不设超时上限——旋转持续到到位(或无定位时按估算时长结束),或被 Stop()/TestStop() 主动中止。 + + /// 到位后保持脚本使能、角速度归零的安定时长(s),让纠偏把队形稳住再撤离。 + public float SettleSec = 0.5f; + + private void ClearScript() + { + var self = PilotDefinition.Self; + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleScriptEnabled = false; + } + + public void Stop() => ClearScript(); + + public override IEnumerable Get() + { + var self = PilotDefinition.Self; + var conf = PilotDefinition.Conf; + + if (conf.MultiVehicleMasterEndpoint != "/") + { + Hedingben.ToastText("车队原地旋转需在主车(主车端点=\"/\")运行", "FleetRotate"); + yield break; + } + + var dir = Math.Sign(TargetDeltaDeg); + if (dir == 0) dir = 1; + var maxOmega = Math.Abs(Omega); + var minOmega = Math.Min(Math.Abs(MinOmega), maxOmega); // 最小不超过最大 + var slowDeg = Math.Max(1e-3f, SlowDeg); // 减速区宽度 + var accel = AccelDegPerSec2; // 缓启动角加速度,仅作用于起步,<=0 关闭 + var targetMag = Math.Abs(TargetDeltaDeg); + + var hasPos = UseDetourHeading; + var prevTh = hasPos ? (float)DetourInterface.getCartLocation().th : 0f; + var startTh = prevTh; + var accumulated = 0f; // 累计带符号转角(deg) + var start = DateTime.Now; + var lastTime = start; + var lastLog = DateTime.MinValue; + var lastCenterLog = DateTime.MinValue; + var cmdMag = 0f; // 当前实际下发角速度大小(deg/s),缓启动从 0 斜坡爬升 + var centerTracking = false; + float centerStartX = 0, centerStartY = 0, centerStartTh = 0; + float centerLastX = 0, centerLastY = 0, centerLastTh = 0, centerMaxDrift = 0; + // 无定位按时长估算时,补上缓启动斜坡少转的等效时间(≈ maxOmega/(2·accel)),使时长更接近目标角。 + var estDuration = maxOmega > 1e-3 ? targetMag / maxOmega : 0; + if (accel > 1e-3) estDuration += maxOmega / (2 * accel); + + DLog.Log( + $"REQUEST target={TargetDeltaDeg:0.0} dir={dir} omega={maxOmega:0.0} accel={accel:0.0} " + + $"slowDeg={slowDeg:0.0} minOmega={minOmega:0.0} useDetourHeading={hasPos} startTh={startTh:0.00} " + + $"estDuration={estDuration:0.00}s syncUseDetour={conf.MultiVehicleSyncUseDetour}", + "FleetRotateDbg"); + + // 使能脚本驱动的原地旋转(mode2)。TickMultiVehicle 后台循环据此执行旋转并广播给从车。 + // 起步从 0 角速度开始,由缓启动斜坡爬升,避免阶跃下发导致队形骤偏/抖动。 + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptMode = 2; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleScriptEnabled = true; + self.MultiVehicleRotateWheelsReady = false; + self.MultiVehicleRotateFleetReady = false; + + DLog.Log("WAIT_ALIGN fleet rotate wheels", "FleetRotateDbg"); + while (!self.MultiVehicleRotateFleetReady) + { + self.MultiVehicleScriptVx = 0; + self.MultiVehicleScriptVy = 0; + self.MultiVehicleScriptMode = 2; + self.MultiVehicleScriptVth = 0; + self.MultiVehicleScriptEnabled = true; + Hedingben.ToastText("车队原地旋转舵轮预对齐中", "FleetRotate"); + yield return true; + } + + float centerStartCarX = 0, centerStartCarY = 0, centerStartCarTh = 0; + if (hasPos) + { + var startPos = DetourInterface.getCartLocation(); + centerStartCarX = (float)startPos.x; + centerStartCarY = (float)startPos.y; + centerStartCarTh = (float)startPos.th; + prevTh = centerStartCarTh; + startTh = prevTh; + if (self.TryGetFleetCenterFromPose(centerStartCarX, centerStartCarY, centerStartCarTh, + out centerStartX, out centerStartY, out centerStartTh)) + { + centerLastX = centerStartX; + centerLastY = centerStartY; + centerLastTh = centerStartTh; + centerMaxDrift = 0; + centerTracking = true; + } + } + accumulated = 0f; + start = DateTime.Now; + lastTime = start; + lastLog = DateTime.MinValue; + lastCenterLog = DateTime.MinValue; + cmdMag = 0f; + DLog.Log( + $"START target={TargetDeltaDeg:0.0} dir={dir} omega={maxOmega:0.0} startTh={startTh:0.00} " + + $"fleetAligned={self.MultiVehicleRotateFleetReady}", + "FleetRotateDbg"); + if (centerTracking) + { + DLog.Log( + $"START center=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + + $"car=({centerStartCarX:0.0},{centerStartCarY:0.0},{centerStartCarTh:0.00}) " + + $"target={TargetDeltaDeg:0.0} omega={maxOmega:0.0}", + "FleetRotateCenterDbg"); + } + + var stopReason = "stop()"; + while (true) + { + var now = DateTime.Now; + var dt = (float)Math.Min(0.2, Math.Max(0, (now - lastTime).TotalSeconds)); + lastTime = now; + var elapsed = (now - start).TotalSeconds; + + float desiredMag; + float curTh = 0f, remaining = 0f, actualRate = 0f; + if (hasPos) + { + var carPos = DetourInterface.getCartLocation(); + curTh = (float)carPos.th; + var step = (float)CommonMath.ThDiff(curTh, prevTh); // 本帧实际转角(逆时针为正) + accumulated += step; + actualRate = dt > 1e-3 ? step / dt : 0f; // 实际角速率(deg/s),用于对比指令 + prevTh = curTh; + + remaining = targetMag - Math.Abs(accumulated); + if (remaining <= ArriveDeg) { stopReason = "arrived"; break; } + + // 减速区:剩余角度 < SlowDeg 时,目标角速度按剩余比例线性降到 MinOmega, + // 使切断指令瞬间残余动量足够小,抑制惯性滑行造成的超调。宽度直观、便于现场调试。 + desiredMag = remaining < slowDeg + ? Math.Max(minOmega, maxOmega * (remaining / slowDeg)) + : maxOmega; + + if (centerTracking && + self.TryGetFleetCenterFromPose((float)carPos.x, (float)carPos.y, (float)carPos.th, + out centerLastX, out centerLastY, out centerLastTh)) + { + var centerDx = centerLastX - centerStartX; + var centerDy = centerLastY - centerStartY; + var centerDrift = (float)Math.Sqrt(centerDx * centerDx + centerDy * centerDy); + centerMaxDrift = Math.Max(centerMaxDrift, centerDrift); + var centerDth = (float)CommonMath.ThDiff(centerLastTh, centerStartTh); + if ((now - lastCenterLog).TotalMilliseconds >= 250) + { + lastCenterLog = now; + DLog.Log( + $"ACTION t={elapsed:0.00}s center=({centerLastX:0.0},{centerLastY:0.0},{centerLastTh:0.00}) " + + $"start=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + + $"drift=({centerDx:0.0},{centerDy:0.0}) dist={centerDrift:0.0} max={centerMaxDrift:0.0} dth={centerDth:0.00} " + + $"cmdW={dir * cmdMag:0.000} actualW={actualRate:0.000} acc={accumulated:0.0} remain={remaining:0.0} " + + $"wheelReady={self.MultiVehicleRotateWheelsReady} fleetReady={self.MultiVehicleRotateFleetReady}", + "FleetRotateCenterDbg"); + } + } + } + else + { + // 无定位:按时长估算,无法测角,目标维持巡航速度到估算时长(仅缓启动整形)。 + desiredMag = maxOmega; + if (elapsed >= estDuration) { stopReason = "estDuration"; break; } + } + + // 缓启动:只对“加速(目标>当前)”按角加速度限斜率,让起步平滑爬升; + // “减速(目标<当前)”跟随上面的减速曲线立即下调,保证及时刹车不超调。 + if (accel > 1e-3 && desiredMag > cmdMag) + cmdMag = Math.Min(desiredMag, cmdMag + accel * dt); + else + cmdMag = desiredMag; + + self.MultiVehicleScriptVth = dir * cmdMag; + + // 落盘诊断(节流~150ms):实际航向/累计转角/实际角速率 vs 指令角速率,定位"开环转速不足"。 + if ((now - lastLog).TotalMilliseconds >= 150) + { + lastLog = now; + DLog.Log( + hasPos + ? $"t={elapsed:0.00}s curTh={curTh:0.00} acc={accumulated:0.0} remain={remaining:0.0} " + + $"cmdW={dir * cmdMag:0.0} actualW={actualRate:0.0} (实际/指令={(Math.Abs(cmdMag) > 1e-3 ? actualRate / (dir * cmdMag) : 0):0.00})" + : $"t={elapsed:0.00}s/{estDuration:0.00}s (无航向反馈,开环按时长) cmdW={dir * cmdMag:0.0}", + "FleetRotateDbg"); + } + + Hedingben.ToastText( + hasPos + ? $"车队原地旋转 目标{TargetDeltaDeg:0.0}° 已转{accumulated:0.0}° 余{targetMag - Math.Abs(accumulated):0.0}° ω={cmdMag:0.0}" + : $"车队原地旋转(无定位,按时长) {elapsed:0.0}/{estDuration:0.0}s ω={cmdMag:0.0}", + "FleetRotate"); + + yield return true; + } + + // 到位:角速度先归零,保持脚本使能让 TickMultiVehicle 的 PI 把队形稳住一小段时间再撤离。 + self.MultiVehicleScriptVth = 0; + var settleEnd = DateTime.Now.AddSeconds(Math.Max(0, SettleSec)); + while (DateTime.Now < settleEnd) + yield return true; + + ClearScript(); + DLog.Log( + $"DONE reason={stopReason} 累计转角={accumulated:0.0}° 目标={TargetDeltaDeg:0.0}° " + + $"用时={(DateTime.Now - start).TotalSeconds:0.00}s useDetourHeading={hasPos}", + "FleetRotateDbg"); + if (centerTracking) + { + var centerDx = centerLastX - centerStartX; + var centerDy = centerLastY - centerStartY; + var centerDrift = (float)Math.Sqrt(centerDx * centerDx + centerDy * centerDy); + var centerDth = (float)CommonMath.ThDiff(centerLastTh, centerStartTh); + DLog.Log( + $"DONE reason={stopReason} center=({centerLastX:0.0},{centerLastY:0.0},{centerLastTh:0.00}) " + + $"start=({centerStartX:0.0},{centerStartY:0.0},{centerStartTh:0.00}) " + + $"drift=({centerDx:0.0},{centerDy:0.0}) dist={centerDrift:0.0} max={centerMaxDrift:0.0} dth={centerDth:0.00} " + + $"acc={accumulated:0.0} target={TargetDeltaDeg:0.0}", + "FleetRotateCenterDbg"); + } + Hedingben.ToastText($"车队原地旋转完成({stopReason}) 累计{accumulated:0.0}°", "FleetRotate"); + } +} + +[MovementTest(name = "车队联动-原地旋转")] +public class FleetRotateInPlaceTest : MovementTest +{ + private FleetRotateInPlace _proc; + private DriveTask _task; + + public override void Test() + { + _proc = new FleetRotateInPlace + { + Omega = PilotDefinition.Conf.FleetRotateOmega, + TargetDeltaDeg = PilotDefinition.Conf.FleetRotateTargetDeltaDeg, + ArriveDeg = PilotDefinition.Conf.FleetRotateArriveDeg, + SlowDeg = PilotDefinition.Conf.FleetRotateSlowDeg, + MinOmega = PilotDefinition.Conf.FleetRotateMinOmega, + AccelDegPerSec2 = PilotDefinition.Conf.FleetRotateAccel, + SettleSec = PilotDefinition.Conf.FleetRotateSettleSec, + UseDetourHeading = PilotDefinition.Conf.FleetRotateUseDetourHeading + }; + _task = new DriveTask(_proc.Get()); + _task.Wait(); + } + + public override void TestStop() + { + _proc?.Stop(); + _task?.Stop(); + } +} + +[MovementTest(name = "车队联动-曲线行走")] +public class FleetCurveWalkTest : MovementTest +{ + private FleetCurveWalk _proc; + private DriveTask _task; + + public override void Test() + { + var self = PilotDefinition.Self; + if (!self.TryGetFleetCenterFromMembers(out var x, out var y, out var th) && + !self.TryGetFleetCenterFromSlam(out x, out y, out th)) + { + DLog.Log("FleetCurveWalkTest abort: failed to read fleet center.", "FleetCurveDbg"); + Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve"); + return; + } + + var pointCount = Math.Max(3, PilotDefinition.Conf.FleetCurveTestControlPointCount); + var controlPoints = new List(); + for (var i = 0; i < pointCount; i++) + controlPoints.Add(UI.GetPoint($"FleetCurve point {i + 1}/{pointCount}")); + var fleetCenter = new Vector2(x, y); + if (Vector2.Distance(fleetCenter, controlPoints[0]) > + Vector2.Distance(fleetCenter, controlPoints[controlPoints.Count - 1])) + controlPoints.Reverse(); + var track = new BezierTrack(controlPoints) + { + Speed = PilotDefinition.Conf.FleetCurveSpeed, + CarDirectionBias = 0f + }; + + _proc = new FleetCurveWalk + { + Track = track, + CurveSpeed = PilotDefinition.Conf.FleetCurveSpeed, + CarDirectionBias = 0f, + SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance, + FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance, + FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed, + SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow, + GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold, + StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec + }; + _task = new DriveTask(_proc.Get()); + _task.Wait(); + } + + public override void TestStop() + { + _proc?.Stop(); + _task?.Stop(); + } +} + +[MovementTest(name = "车队联动-自动蟹行")] +public class FleetCrabWalkTest : MovementTest +{ + private FleetCrabWalk _proc; + private DriveTask _task; + + public override void Test() + { + _proc = new FleetCrabWalk + { + CrabAngleDeg = PilotDefinition.Conf.FleetCrabAngleDeg, + BodyToPathAngleDeg = PilotDefinition.Conf.FleetCrabAngleDeg, + CrabLengthMm = PilotDefinition.Conf.FleetCrabLengthMm, + CrabSpeed = PilotDefinition.Conf.FleetCrabSpeed, + FleetCrabAccel = PilotDefinition.Conf.FleetCrabAccel, + FleetCrabStartAccel = PilotDefinition.Conf.FleetCrabStartAccel, + FleetCrabSlowDistance = PilotDefinition.Conf.FleetCrabSlowDistance, + FleetCrabFinishDistance = PilotDefinition.Conf.FleetCrabFinishDistance, + FleetCrabFinishSpeed = PilotDefinition.Conf.FleetCrabFinishSpeed, + FleetCrabSlowingPow = PilotDefinition.Conf.FleetCrabSlowingPow, + GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold + }; + _task = new DriveTask(_proc.Get()); + _task.Wait(); + } + + public override void TestStop() + { + _proc?.Stop(); + _task?.Stop(); + } +} + +// ===== 调用 Playground WebAPI 瞬移小车(前移 / 左移 / 旋转)===== +// 平移/旋转量在 Fields 面板配置:WebApiTranslateMm(默认100mm)、WebApiRotateDeg(默认5度)。 + +[MovementTest(name = "多舵轮-WebAPI前移")] +public class WebApiForwardMoveTest : MovementTest +{ + public override void Test() + { + var url = PilotDefinition.Conf.PlaygroundWebApiUrl; + var name = PilotDefinition.Conf.PlaygroundRobotName; + var d = PilotDefinition.Conf.WebApiTranslateMm; + + var pose = PlaygroundWebApi.GetPose(url, name); + // 车体系前向 (d, 0) 变换到世界系:车头方向即朝向 yaw + var dst = CommonMath.Transform2D(new Vector2(pose.X, pose.Y), pose.YawDeg, new Vector2(d, 0)); + PlaygroundWebApi.Move(url, name, dst.X, dst.Y, pose.YawDeg); + Hedingben.ToastText($"前移 {d:f0}mm -> ({dst.X:f0},{dst.Y:f0})", "WebApiForward"); + } + + public override void TestStop() + { + } +} + +[MovementTest(name = "多舵轮-WebAPI左移")] +public class WebApiLeftMoveTest : MovementTest +{ + public override void Test() + { + var url = PilotDefinition.Conf.PlaygroundWebApiUrl; + var name = PilotDefinition.Conf.PlaygroundRobotName; + var d = PilotDefinition.Conf.WebApiTranslateMm; + + var pose = PlaygroundWebApi.GetPose(url, name); + // 车体系左向 (0, d) 变换到世界系(车体 +Y 即左侧) + var dst = CommonMath.Transform2D(new Vector2(pose.X, pose.Y), pose.YawDeg, new Vector2(0, d)); + PlaygroundWebApi.Move(url, name, dst.X, dst.Y, pose.YawDeg); + Hedingben.ToastText($"左移 {d:f0}mm -> ({dst.X:f0},{dst.Y:f0})", "WebApiLeft"); + } + + public override void TestStop() + { + } +} + +[MovementTest(name = "多舵轮-WebAPI旋转")] +public class WebApiRotateTest : MovementTest +{ + public override void Test() + { + var url = PilotDefinition.Conf.PlaygroundWebApiUrl; + var name = PilotDefinition.Conf.PlaygroundRobotName; + var deg = PilotDefinition.Conf.WebApiRotateDeg; + + var pose = PlaygroundWebApi.GetPose(url, name); + var ny = pose.YawDeg + deg; // 逆时针为正 + PlaygroundWebApi.Move(url, name, pose.X, pose.Y, ny); + Hedingben.ToastText($"旋转 {deg:f1}° -> {ny:f1}°", "WebApiRotate"); + } + + public override void TestStop() + { + } +} + +[MovementTest(name = "多舵轮-WebAPI恢复运动")] +public class WebApiMotionResumeTest : MovementTest +{ + public override void Test() + { + var url = PilotDefinition.Conf.PlaygroundWebApiUrl; + PlaygroundWebApi.ResumeMotion(url); + Hedingben.ToastText("已恢复车辆运动", "WebApiMotion"); + } + + public override void TestStop() + { + } +} + +[MovementTest(name = "多舵轮-WebAPI暂停运动")] +public class WebApiMotionPauseTest : MovementTest +{ + public override void Test() + { + var url = PilotDefinition.Conf.PlaygroundWebApiUrl; + PlaygroundWebApi.PauseMotion(url); // 默认 zero 模式:反馈归零 + Hedingben.ToastText("已暂停车辆运动 (zero)", "WebApiMotion"); + } + + public override void TestStop() + { + } +} diff --git a/.task8-sweep/Movements.cs b/.task8-sweep/Movements.cs new file mode 100644 index 0000000..6dd4a7c --- /dev/null +++ b/.task8-sweep/Movements.cs @@ -0,0 +1,326 @@ +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using ClumsyCore.Sensors; +using ClumsyCore.Utilities; +using ClumsyDance.ClumsyWalk.Detectors; +using ClumsyDance.Sensors; +using CommonUsage.Chassis; +using FundamentalLib; +using MDCSToolBox.Clumsy.Calibration; +using MDCSToolBox.Clumsy.HighLevelSecurity; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MDCSToolBox.Clumsy.Tracks; +using MDCSToolBox.Commons; +using MDCSToolBox.Commons.Controllers; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Net.Http; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading; +using static ClumsyCore.DTools.Painter; + +namespace MultiWheelC +{ + public class MultiWheelRotateInPlace : MovementDefinition + { + /// + /// 旋转目标角度 + /// + public float AngleTarget; + + public float MaxSpeed; + + public Func ThetaReader = () => (float)DetourInterface.getCartLocation().th; + + public MultiWheelChassis Chassis = (MultiWheelChassis)PilotDefinition.Chassis; + + public Func PidparamsRead = () => new PIDParams() { }; + + public PIDController thPid; + + private static float RangeAngle(float theta) + { + return (float)(theta - Math.Round(theta / 360.0f) * 360); + } + + public override IEnumerable Get() + { + var targetAngle = RangeAngle(AngleTarget); + var p = PidparamsRead(); + thPid = new PIDController(ThetaReader, p.Kp); + thPid.ChangeParameters(p.Kp, p.Ki, p.Kd, p.MaxI, p.DeadZone, p.OutputUpperThreshold, p.SpeedAccPerSec); + + DateTime lastTime = DateTime.Now; + while (true) + { + var s = thPid.GetResponse(targetAngle, true); + Console.WriteLine($"s:{s} AngleTarget:{AngleTarget}"); + Chassis.SendXYThSpeed(0, 0, s); + lastTime = DateTime.Now; + if (thPid.IsArrived()) break; + yield return true; + } + Chassis.SendXYThSpeed(0, 0, 0); + Console.WriteLine($"final rotate to {targetAngle}"); + } + } + + + public class ClampToTarget : MovementDefinition + { + public float LeftClampTarget; + public float RightClampTarget; + + public float MaxClampSpeed = PilotDefinition.Conf.MaxClampSpeed; + public float ClampKp = PilotDefinition.Conf.ClampControlKp; + public float ClampKi = PilotDefinition.Conf.ClampControlKi; + public float ClampKd = PilotDefinition.Conf.ClampControlKd; + public float ClampMaxI = PilotDefinition.Conf.ClampControlMaxI; + public float ClampSpeedAcc = PilotDefinition.Conf.ClampControlSpeedAcc; + public float ClampDeadZone = PilotDefinition.Conf.ClampControlDeadZone; + private PIDController leftpid, rightpid; + + public override IEnumerable Get() + { + leftpid = new PIDController(() => PilotDefinition.Self.ActualPosLeftArm, ClampKp, ClampKi, ClampKd, + ClampMaxI, ClampDeadZone, MaxClampSpeed) + { SpeedAccPerSec = ClampSpeedAcc }; + + rightpid = new PIDController(() => PilotDefinition.Self.ActualPosRightArm, ClampKp, ClampKi, ClampKd, + ClampMaxI, ClampDeadZone, MaxClampSpeed) + { SpeedAccPerSec = ClampSpeedAcc }; + + while (true) + { + var leftspeed = leftpid.GetResponse(LeftClampTarget); + var rightspeed = rightpid.GetResponse(RightClampTarget); + Console.WriteLine($"left arm speed:{leftspeed} right arm speed:{rightspeed}"); + PilotDefinition.Self.SpeedLeftArm = leftspeed; + PilotDefinition.Self.SpeedRightArm = rightspeed; + if (leftpid.IsArrived()) PilotDefinition.Self.SpeedLeftArm = 0; + if (rightpid.IsArrived()) PilotDefinition.Self.SpeedRightArm = 0; + + if (leftpid.IsArrived() && rightpid.IsArrived()) break; + yield return true; + } + + PilotDefinition.Self.SpeedLeftArm = 0; + PilotDefinition.Self.SpeedRightArm = 0; + Console.WriteLine($"left clamp to target:{LeftClampTarget} right clamp to target:{RightClampTarget}"); + } + } + + public class Sleep : MovementDefinition + { + public float Second = 2; + public override IEnumerable Get() + { + var start = DateTime.Now; + while ((DateTime.Now-start).TotalSeconds LeaveSrcFunction = null; + public Painter painter = UI.GetPainter("Line", false); + public override IEnumerable Get() + { + var curpose = DetourInterface.getCartLocation(); + Console.WriteLine($"curpose.th:{curpose.th}"); + var src = new Vector2((float)curpose.x, (float)curpose.y); + var dst = new Vector2((float)curpose.x + LineDistance * (float)Math.Cos(curpose.th), + (float)curpose.y + LineDistance * (float)Math.Sin(curpose.th)); + Console.WriteLine($"src:{src.X} {src.Y}"); + Console.WriteLine($"dst:{dst.X} {dst.Y}"); + painter.DrawLine(Color.Green, src.X, src.Y, dst.X, dst.Y, width: 3); + + var tracker = new ChassisController().Get(); + var linePath = new LineTrack(src, dst) { CarDirectionBias = LineDistance > 0 ? 0 : 180 }; + tracker.AddTrack(linePath); + var _dt = new DriveTask(tracker.Track()); + _dt.Wait(); + if (SrcId != -1 && LeaveSrcFunction != null) + { + LeaveSrcFunction(SrcId); + DLog.Log($"释放放车点{SrcId}", "TireFollowing"); + } + yield return false; + } + } + + //在世界坐标系下,从路径起点追踪到终点并停车 + public class DstTracker : MovementDefinition + { + public Vector2 Src; + public Vector2 Dst; + public float CarDirectionBias = 0f; + public Painter Painter = UI.GetPainter("DstTracker"); + + public float InitialSendSpeed = 0; + + public override IEnumerable Get() + { + Console.WriteLine($"DstTracker src:({Src.X:F2}, {Src.Y:F2}) dst:({Dst.X:F2}, {Dst.Y:F2})"); + Painter.DrawLine(Color.Cyan, Src.X, Src.Y, Dst.X, Dst.Y, width: 3); + + var tracker = new ChassisController().Get(); + + if (InitialSendSpeed != 0) + { + tracker.SkipInitialRotate = true; + tracker.InitialSendSpeed = InitialSendSpeed; + } + + var linePath = new LineTrack(Src, Dst) { CarDirectionBias = CarDirectionBias, Speed = PilotDefinition.Conf.DstTrackerMaxSpeed }; + tracker.AddTrack(linePath); + var task = new DriveTask(tracker.Track()); + task.Wait(); + + // 到点后兜底停车 + var chassis = (MultiWheelChassis)PilotDefinition.Chassis; + chassis.SendXYThSpeed(0f, 0f, 0f); + yield return false; + } + } + + //直线行走基于轮里程 + public class LineTracking : MovementDefinition + { + public float Target; + public float MaxSpeed = PilotDefinition.Conf.LineTrackMaxSpeed; + public float Kp = PilotDefinition.Conf.LineTrackKp; + public float Ki = PilotDefinition.Conf.LineTrackKi; + public float Kd = PilotDefinition.Conf.LineTrackKd; + public float DeadZone = PilotDefinition.Conf.LineTrackDeadZone; + public int SrcId = -1; + public int DstId = -1; + public Action LeaveSrcFunction = null; + private PIDController pid; + // 末段衔接:接近目标后不再让 PID 把速度降到 0,保留一个接力速度给后续动作接管 + public bool EnableHandover = false; + public float HandoverDistance = 80f; // mm + public float HandoverSpeed = 0.15f; // m/s + + public override IEnumerable Get() + { + + pid = new PIDController(() => + (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2, + Kp, Ki, Kd, 0, DeadZone, MaxSpeed) + { SpeedAccPerSec = MaxSpeed / 2f }; + + var chassis = (MultiWheelChassis)PilotDefinition.Chassis; + //chassis.SetOriginBias(0, 0, 0); + DLog.Log($"直线行驶距离:{Target}", "TireFollowing"); + while (true) + { + var current = (PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2; + var remain = Target - current; + if (EnableHandover && Math.Abs(remain) <= Math.Max(1f, HandoverDistance)) + { + var handoverSign = Math.Sign(remain); + if (handoverSign == 0) handoverSign = 1; + var handoverSpeed = Math.Abs(HandoverSpeed) * handoverSign; + Console.WriteLine($"handover speed: {handoverSpeed:F3}, remain: {remain:F2}"); + chassis.SendXYThSpeed(handoverSpeed, 0, 0); + // 保留一拍接力速度,让后续 DstTracker 无缝接管 + yield return true; + break; + } + var speed = pid.GetResponse(Target); + Console.WriteLine($"output: {speed} current: {(PilotDefinition.Self.LFLActualPos + PilotDefinition.Self.LFRActualPos) / 2}"); + + chassis.SendXYThSpeed(speed, 0, 0); + + if (pid.IsArrived()) break; + yield return true; + } + if (SrcId != -1 && LeaveSrcFunction != null) + { + LeaveSrcFunction(SrcId); + DLog.Log($"释放放车点{SrcId}", "TireFollowing"); + } + yield return false; + } + } + + public class DriverAble : MovementDefinition + { + public int WaitTimeoutMs = 2000; + public int PollIntervalMs = 50; + + public override IEnumerable Get() + { + Console.WriteLine("驱动器上使能"); + PilotDefinition.Self.ResetFromC = true; + + var start = DateTime.Now; + var timeoutMs = Math.Max(0, WaitTimeoutMs); + var pollMs = Math.Max(1, PollIntervalMs); + var success = PilotDefinition.Self.WheelAbleState; + while (!success && (DateTime.Now - start).TotalMilliseconds < timeoutMs) + { + Thread.Sleep(pollMs); + success = PilotDefinition.Self.WheelAbleState; + if (!success) yield return true; + } + + PilotDefinition.Self.ResetFromC = false; + if (success) + Console.WriteLine($"驱动器上使能完成,WheelAbleState={PilotDefinition.Self.WheelAbleState}"); + else + Console.WriteLine($"驱动器上使能超时,WheelAbleState={PilotDefinition.Self.WheelAbleState},等待{timeoutMs}ms"); + yield return false; + } + } + + public class DriverDisable : MovementDefinition + { + public int WaitTimeoutMs = 3000; + public int PollIntervalMs = 20; + + public override IEnumerable Get() + { + Console.WriteLine("驱动器下使能"); + PilotDefinition.Self.DisableFromC = true; + + var start = DateTime.Now; + var timeoutMs = Math.Max(0, WaitTimeoutMs); + var pollMs = Math.Max(1, PollIntervalMs); + var success = !PilotDefinition.Self.WheelAbleState; + while (!success && (DateTime.Now - start).TotalMilliseconds < timeoutMs) + { + Thread.Sleep(pollMs); + success = !PilotDefinition.Self.WheelAbleState; + if (!success) yield return true; + } + + PilotDefinition.Self.DisableFromC = false; + if (success) + Console.WriteLine($"驱动器下使能完成,WheelAbleState={PilotDefinition.Self.WheelAbleState}"); + else + Console.WriteLine($"驱动器下使能超时,WheelAbleState={PilotDefinition.Self.WheelAbleState},等待{timeoutMs}ms"); + yield return false; + } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs new file mode 100644 index 0000000..74b55ef --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs @@ -0,0 +1,63 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 最终粗路径上的一个稠密连续点;长度、净空和位置使用 m,航向使用 rad,曲率使用 1/m。 +public sealed class CoarsePathPoint +{ + /// + /// 创建粗路径点。 + /// 参数:xMeters、yMeters 为世界坐标 m;headingRadians 与 unwrappedHeadingRadians 为航向 rad;arcLengthMeters 和 bodyClearanceMeters 为 m;vehicleCurvaturePerMeter 为 1/m。 + /// + public CoarsePathPoint( + double xMeters, + double yMeters, + double headingRadians, + double unwrappedHeadingRadians, + double arcLengthMeters, + TravelDirection direction, + double vehicleCurvaturePerMeter, + double bodyClearanceMeters, + bool isGearSwitchPoint, + CoarsePathPointSource source) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + UnwrappedHeading = unwrappedHeadingRadians; + ArcLength = arcLengthMeters; + Direction = direction; + VehicleCurvature = vehicleCurvaturePerMeter; + BodyClearance = bodyClearanceMeters; + IsGearSwitchPoint = isGearSwitchPoint; + Source = source; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 归一化后可用于几何查询的车头航向,单位 rad。 + public double Heading { get; } + + /// 跨越 ±π 后仍连续的车头航向,单位 rad。 + public double UnwrappedHeading { get; } + + /// 从路径起点累计的弧长,单位 m;必须非负且不递减。 + public double ArcLength { get; } + + /// 从上一点运动到当前点所在方向段的行驶方向。 + public TravelDirection Direction { get; } + + /// 车辆在此点采用的恒曲率原语曲率,单位 1/m。 + public double VehicleCurvature { get; } + + /// 扩大车体到障碍物的保守净空下界,单位 m。 + public double BodyClearance { get; } + + /// 此点是否为新方向段开始的换向点。 + public bool IsGearSwitchPoint { get; } + + /// 此点由起点、普通原语或终点截断产生的来源。 + public CoarsePathPointSource Source { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs new file mode 100644 index 0000000..1e4ad26 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径点在搜索与原语重建中的产生来源。 +public enum CoarsePathPointSource +{ + /// 请求提供的起始位姿。 + Start, + + /// 未截断恒曲率运动原语的内部积分点。 + MotionPrimitive, + + /// 首次满足目标容差而在原语内部截断的终点积分点。 + GoalTruncation, +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs new file mode 100644 index 0000000..351c690 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 终点候选最后一个连续运动段的方向约束。 +public enum GoalDirectionConstraint +{ + /// 不限制进入终点的运动方向。 + Any, + + /// 必须以前进方向进入终点。 + Forward, + + /// 必须以倒车方向进入终点。 + Reverse, +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs new file mode 100644 index 0000000..dc361f0 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs @@ -0,0 +1,83 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// Hybrid A* 粗路径搜索的可调配置。 +/// 长度和距离使用 m,航向使用 rad,曲率使用 1/m;各项有效范围由规划器在执行前校验。 +/// +public sealed class HybridAStarConfiguration +{ + /// 创建采用 P0 固定安全边界和代价权重的默认配置。 + public HybridAStarConfiguration() + { + PrimitiveLengthMeters = 0.50d; + IntegrationStepMeters = 0.05d; + MaximumCollisionCheckStepMeters = 0.025d; + HeadingResolutionRadians = Math.PI / 36d; + CurvatureLevelCount = 5; + GoalPositionToleranceMeters = 0.15d; + GoalHeadingToleranceRadians = Math.PI / 36d; + MaximumExpandedNodes = 200000; + SearchTimeout = TimeSpan.FromSeconds(5d); + HeuristicWeight = 1d; + ReverseCostMultiplier = 1.5d; + GearSwitchPenaltyMeters = 1d; + CurvatureMagnitudeWeight = 0.10d; + CurvatureChangePenaltyMetersPerLevel = 0.05d; + ClearanceCostWeight = 0.20d; + ClearanceCostDistanceMeters = 0.50d; + AllowReverse = true; + } + + /// 单个恒曲率原语的最大行驶长度,单位 m。 + public double PrimitiveLengthMeters { get; set; } + + /// 原语内部输出积分点之间允许的最大弧长,单位 m。 + public double IntegrationStepMeters { get; set; } + + /// 连续碰撞检查允许的最大车辆中心位移,单位 m;实际值还受地图分辨率限制。 + public double MaximumCollisionCheckStepMeters { get; set; } + + /// 搜索状态离散所使用的航向格宽,单位 rad。 + public double HeadingResolutionRadians { get; set; } + + /// 从最大负曲率到最大正曲率的离散曲率等级数。 + public int CurvatureLevelCount { get; set; } + + /// 终点位置允许的欧氏距离误差,单位 m。 + public double GoalPositionToleranceMeters { get; set; } + + /// 终点车头航向允许的最小环形角度误差,单位 rad。 + public double GoalHeadingToleranceRadians { get; set; } + + /// 单次搜索允许扩展的最大节点数。 + public int MaximumExpandedNodes { get; set; } + + /// 单次搜索允许消耗的最长时间;超出后返回 + public TimeSpan SearchTimeout { get; set; } + + /// 二维绕障启发式的权重;1 表示不额外放大。 + public double HeuristicWeight { get; set; } + + /// 倒车原语长度代价相对前进原语的倍率。 + public double ReverseCostMultiplier { get; set; } + + /// 相邻原语发生换向时增加的等效距离代价,单位 m。 + public double GearSwitchPenaltyMeters { get; set; } + + /// 曲率绝对值对应的无量纲代价权重。 + public double CurvatureMagnitudeWeight { get; set; } + + /// 相邻曲率等级每变化一级增加的等效距离代价,单位 m。 + public double CurvatureChangePenaltyMetersPerLevel { get; set; } + + /// 车体保守净空不足时增加的无量纲代价权重。 + public double ClearanceCostWeight { get; set; } + + /// 计算净空代价时视为足够安全的车体保守净空,单位 m。 + public double ClearanceCostDistanceMeters { get; set; } + + /// 是否允许生成倒车原语;false 时搜索只生成前进原语。 + public bool AllowReverse { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs new file mode 100644 index 0000000..5a6a0e9 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs @@ -0,0 +1,37 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径中方向一致的一段连续点范围;索引两端均包含在段内。 +public sealed class PathSegment +{ + /// + /// 创建方向分段。 + /// 参数:segmentIndex 为从零开始的段序号;startIndex 与 endIndex 为 的包含式索引;两个换向标记描述段首或段尾是否位于换向对。 + /// + public PathSegment(int segmentIndex, TravelDirection direction, int startIndex, int endIndex, bool startsAtGearSwitch, bool endsAtGearSwitch) + { + SegmentIndex = segmentIndex; + Direction = direction; + StartIndex = startIndex; + EndIndex = endIndex; + StartsAtGearSwitch = startsAtGearSwitch; + EndsAtGearSwitch = endsAtGearSwitch; + } + + /// 从零开始的方向段序号。 + public int SegmentIndex { get; } + + /// 此段所有连续运动点对应的行驶方向。 + public TravelDirection Direction { get; } + + /// 此段在 中的起始索引,包含该点。 + public int StartIndex { get; } + + /// 此段在 中的结束索引,包含该点。 + public int EndIndex { get; } + + /// 此段首点是否为换向后保留的新方向点。 + public bool StartsAtGearSwitch { get; } + + /// 此段尾点是否紧邻下一方向段的换向对。 + public bool EndsAtGearSwitch { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs new file mode 100644 index 0000000..8ec8ea3 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs @@ -0,0 +1,65 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 一次规划的只读统计与终止说明;长度和净空单位为 m。 +public sealed class PlanningDiagnostics +{ + /// + /// 创建规划统计快照。 + /// 参数中的节点数量均为非负计数;pathLengthMeters 和 minimumBodyClearanceMeters 单位为 m;elapsed 为总耗时;pathSearchElapsed 为地图和起终点预检通过后的路径产出耗时;terminationReason 为可读终止说明,可为 null。 + /// + public PlanningDiagnostics( + int expandedNodeCount = 0, + int generatedNodeCount = 0, + int reopenedNodeCount = 0, + int staleOpenListEntryCount = 0, + int peakOpenListCount = 0, + double pathLengthMeters = 0d, + double minimumBodyClearanceMeters = 0d, + TimeSpan elapsed = default(TimeSpan), + string terminationReason = null, + TimeSpan pathSearchElapsed = default(TimeSpan)) + { + ExpandedNodeCount = expandedNodeCount; + GeneratedNodeCount = generatedNodeCount; + ReopenedNodeCount = reopenedNodeCount; + StaleOpenListEntryCount = staleOpenListEntryCount; + PeakOpenListCount = peakOpenListCount; + PathLengthMeters = pathLengthMeters; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + Elapsed = elapsed; + PathSearchElapsed = pathSearchElapsed; + TerminationReason = terminationReason ?? string.Empty; + } + + /// 从 Open List 取出并真正扩展的节点数量。 + public int ExpandedNodeCount { get; } + + /// 生成并尝试加入搜索状态的节点数量。 + public int GeneratedNodeCount { get; } + + /// 以严格更小代价到达同一离散键而重新打开的节点数量。 + public int ReopenedNodeCount { get; } + + /// 从 Open List 取出后因已有更优条目而丢弃的陈旧堆条目数量。 + public int StaleOpenListEntryCount { get; } + + /// 搜索期间 Open List 同时容纳的最大有效或待丢弃条目数量。 + public int PeakOpenListCount { get; } + + /// 成功路径的累计弧长,单位 m;失败结果通常为 0。 + public double PathLengthMeters { get; } + + /// 成功路径所有点中车体保守净空下界的最小值,单位 m;失败结果通常为 0。 + public double MinimumBodyClearanceMeters { get; } + + /// 从规划入口到返回结果的总耗时。 + public TimeSpan Elapsed { get; } + + /// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时;不含建图,搜索前失败时为零。 + public TimeSpan PathSearchElapsed { get; } + + /// 面向调用方的终止原因;成功时可为空字符串。 + public string TerminationReason { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs new file mode 100644 index 0000000..057789f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs @@ -0,0 +1,41 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 已建规划地图上的一次 Hybrid A* 请求。 +/// 本对象只接受不可变 快照,不包含地图构建器、传感器、UI 或调试对象。 +/// +public sealed class PlanningRequest +{ + /// 创建空规划请求。调用规划器前必须提供地图、起点、终点、车辆和配置。 + public PlanningRequest() + { + StartVehicleCurvature = 0d; + GoalDirection = GoalDirectionConstraint.Any; + } + + /// 本次搜索唯一允许查询的不可变规划地图快照;位置查询单位为 m。 + public PlanningGridMap Map { get; set; } + + /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; set; } + + /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 + public Pose2D Goal { get; set; } + + /// 车辆几何、余量和曲率限制;尺寸单位 m,曲率单位 1/m。 + public VehicleParameters Vehicle { get; set; } + + /// 搜索步长、离散、终点容差、代价和资源上限配置。 + public HybridAStarConfiguration Configuration { get; set; } + + /// 车辆起步时的转向曲率,单位 1/m;默认值为 0。 + public double StartVehicleCurvature { get; set; } + + /// 起步方向约束;null 表示可从前进或倒车开始。 + public TravelDirection? StartDirection { get; set; } + + /// 目标进入方向约束;默认值为 + public GoalDirectionConstraint GoalDirection { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs new file mode 100644 index 0000000..c69548f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 一次粗路径规划的最终不可变结果。 +/// 只有 时才携带非空路径与方向分段;其他状态始终返回空只读集合。 +/// +public sealed class PlanningResult +{ + private static readonly IReadOnlyList EmptyPath = new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptySegments = new ReadOnlyCollection(new List()); + + private PlanningResult(PlanningStatus status, PlanningDiagnostics diagnostics, IReadOnlyList path, IReadOnlyList segments) + { + Status = status; + Diagnostics = diagnostics ?? new PlanningDiagnostics(terminationReason: "未提供诊断信息。"); + Path = path; + Segments = segments; + } + + /// 规划最终状态;只有 可以发布路径。 + public PlanningStatus Status { get; } + + /// 节点、耗时、路径长度、净空和终止原因统计;始终非空。 + public PlanningDiagnostics Diagnostics { get; } + + /// 成功时的稠密粗路径;失败时为不可修改的空集合。 + public IReadOnlyList Path { get; } + + /// 成功时覆盖 的包含式方向分段;失败时为不可修改的空集合。 + public IReadOnlyList Segments { get; } + + /// + /// 创建成功结果。 + /// 参数:path 与 segments 必须均为非空;diagnostics 为本次规划的统计快照。参数不符合要求时抛出 ,防止以成功状态发布不完整路径。 + /// + public static PlanningResult Success(IReadOnlyList path, IReadOnlyList segments, PlanningDiagnostics diagnostics) + { + if (path == null || path.Count == 0) + throw new ArgumentException("Successful planning results require a non-empty path.", nameof(path)); + if (segments == null || segments.Count == 0) + throw new ArgumentException("Successful planning results require non-empty segments.", nameof(segments)); + return new PlanningResult(PlanningStatus.Success, diagnostics, CopyReadOnly(path), CopyReadOnly(segments)); + } + + /// + /// 创建失败、取消或资源受限结果。 + /// 参数:status 不能为 ;diagnostics 会原样保留。返回结果的路径与分段始终为空只读集合。 + /// + public static PlanningResult Failure(PlanningStatus status, PlanningDiagnostics diagnostics) + { + if (status == PlanningStatus.Success) + throw new ArgumentException("Use Success to create a successful planning result.", nameof(status)); + return new PlanningResult(status, diagnostics, EmptyPath, EmptySegments); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs new file mode 100644 index 0000000..26d5fea --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs @@ -0,0 +1,56 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径规划的最终状态;除 外均不发布路径或方向分段。 +public enum PlanningStatus +{ + /// 已得到并通过最终连续碰撞复核的完整粗路径。 + Success, + + /// 在搜索扩展检查点收到取消请求。 + Cancelled, + + /// 请求对象或其必要成员为空,或包含不符合基本契约的值。 + InvalidRequest, + + /// 请求提供的地图对象不满足规划器的结构要求。 + InvalidMap, + + /// 地图快照尚未准备好参与规划;应查看地图的阻止原因。 + MapNotReady, + + /// 车辆尺寸、安全余量或曲率限制无效。 + InvalidVehicleParameters, + + /// 原语、离散、代价、容差或资源限制配置无效。 + InvalidCurvatureConfiguration, + + /// 起始车辆几何中心或扩大车体不在地图范围内。 + StartOutsideMap, + + /// 起始扩大车体与地图障碍物相交或擦边。 + StartInCollision, + + /// 目标车辆几何中心或扩大车体不在地图范围内。 + GoalOutsideMap, + + /// 目标扩大车体与地图障碍物相交或擦边。 + GoalInCollision, + + /// 搜索达到 限制。 + SearchTimeout, + + /// 搜索达到 限制。 + SearchNodeLimitExceeded, + + /// Open List 已耗尽,或二维启发式证明目标不可达。 + NoFeasiblePath, + + /// 搜索成功节点无法按父链回溯为完整路径。 + BacktrackingFailed, + + /// 回溯路径未通过连续碰撞、终点或输出不变量复核。 + FinalValidationFailed, + + /// 规划内部发生未预期错误;不会发布部分路径。 + InternalError, +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs new file mode 100644 index 0000000..77af135 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs @@ -0,0 +1,28 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 粗路径规划使用的二维连续位姿。 +/// 位置以世界坐标 m 表示,航向以 rad 表示;此值对象不在构造时归一化航向,调用方可保留展开航向。 +/// +public sealed class Pose2D +{ + /// + /// 创建二维位姿。 + /// 参数:xMeters、yMeters 为世界坐标,单位 m;headingRadians 为车头航向,单位 rad。 + /// + public Pose2D(double xMeters, double yMeters, double headingRadians) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 车头航向,单位 rad;可以是已展开的连续航向。 + public double Heading { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs new file mode 100644 index 0000000..8bf17dc --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 连续运动段的行驶方向。 +public enum TravelDirection +{ + /// 沿车辆车头方向前进。 + Forward, + + /// 与车辆车头方向相反地倒车。 + Reverse, +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs new file mode 100644 index 0000000..286f928 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs @@ -0,0 +1,28 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 车辆几何与运动学参数。 +/// 所有几何尺寸均以车辆几何中心为 参考点,单位为 m;曲率单位为 1/m。 +/// +public sealed class VehicleParameters +{ + /// 创建空车辆参数。调用规划器前必须填写有效的几何尺寸和至少一种曲率限制。 + public VehicleParameters() + { + } + + /// 车辆本体长度,单位 m;不含 + public double LengthMeters { get; set; } + + /// 车辆本体宽度,单位 m;不含 + public double WidthMeters { get; set; } + + /// 碰撞检查时加在车体四周的安全余量,单位 m;不会写入地图。 + public double SafetyMarginMeters { get; set; } + + /// 车辆允许的最大绝对曲率,单位 1/m;null 表示由 提供限制。 + public double? MaximumCurvaturePerMeter { get; set; } + + /// 车辆允许的最小转弯半径,单位 m;null 表示由 提供限制。 + public double? MinimumTurningRadiusMeters { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs new file mode 100644 index 0000000..78b4685 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs @@ -0,0 +1,45 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次从地图输入到 Hybrid A* 粗路径输出的完整业务请求。 +/// 地图边界、分辨率和障碍物几何位于 中并使用 mm;位姿和车辆几何使用 m,航向使用 rad,曲率使用 1/m。 +/// +public sealed class CoarsePathPlanningJob +{ + /// 创建默认目标方向、起步曲率和关闭调试旁路的业务请求。 + public CoarsePathPlanningJob() + { + StartVehicleCurvature = 0d; + GoalDirection = GoalDirectionConstraint.Any; + DebugOptions = new PlanningDebugOptions(); + } + + /// 本次唯一建图输入;边界、分辨率和障碍物几何均遵循 Map 模块的 mm 契约。 + public PlanningMapRequest MapRequest { get; set; } + + /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; set; } + + /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 + public Pose2D Goal { get; set; } + + /// 车辆尺寸、安全余量和曲率限制;尺寸单位 m,曲率单位 1/m。 + public VehicleParameters Vehicle { get; set; } + + /// 原语、离散、容差、代价和资源上限配置。 + public HybridAStarConfiguration Configuration { get; set; } + + /// 车辆起步曲率,单位 1/m;默认值为 0。 + public double StartVehicleCurvature { get; set; } + + /// 起步方向约束;null 表示可从前进或倒车开始。 + public TravelDirection? StartDirection { get; set; } + + /// 目标进入方向约束;默认值为 + public GoalDirectionConstraint GoalDirection { get; set; } + + /// 可选调试旁路配置;默认关闭且使用空接收器,不参与地图或路径计算。 + public PlanningDebugOptions DebugOptions { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs new file mode 100644 index 0000000..b96208d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次粗路径业务编排的不可变结果。 +/// 始终同时保留地图构建结果和规划结果;调试旁路消息只用于诊断,不会修改地图或规划内容。 +/// +public sealed class CoarsePathPlanningJobResult +{ + /// + /// 创建业务编排结果。 + /// 参数:mapResult 和 planningResult 均不能为空;debugDiagnostics 可为 null,返回时会转换为只读字符串集合。 + /// + public CoarsePathPlanningJobResult(PlanningMapBuildResult mapResult, PlanningResult planningResult, + IReadOnlyList debugDiagnostics = null) + { + MapResult = mapResult ?? throw new ArgumentNullException(nameof(mapResult)); + PlanningResult = planningResult ?? throw new ArgumentNullException(nameof(planningResult)); + DebugDiagnostics = CopyDiagnostics(debugDiagnostics); + } + + /// 本次调用的地图创建结果;失败时读取 + public PlanningMapBuildResult MapResult { get; } + + /// 本次调用的粗路径结果;地图创建失败时为无路径的失败状态。 + public PlanningResult PlanningResult { get; } + + /// 调试旁路的非致命诊断信息;为空时表示未启用、未发生异常或无额外调试消息。 + public IReadOnlyList DebugDiagnostics { get; } + + private static IReadOnlyList CopyDiagnostics(IReadOnlyList source) + { + if (source == null || source.Count == 0) + return new ReadOnlyCollection(new List()); + + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index] ?? string.Empty); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs new file mode 100644 index 0000000..a4e5926 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 从 到 Hybrid A* 粗路径的一次调用服务。 +/// 服务生命周期内长期持有同一个地图工厂和规划器,以保留地图缓存并避免将 UI、传感器或调试依赖带入规划核心。 +/// +public sealed class CoarsePathPlanningService +{ + private readonly PlanningMapFactory _mapFactory; + private readonly HybridAStarPlanner _planner; + + /// 创建长期复用默认地图工厂与 Hybrid A* 规划器的服务。 + public CoarsePathPlanningService() + : this(new PlanningMapFactory(), new HybridAStarPlanner()) + { + } + + internal CoarsePathPlanningService(PlanningMapFactory mapFactory, HybridAStarPlanner planner) + { + _mapFactory = mapFactory ?? throw new ArgumentNullException(nameof(mapFactory)); + _planner = planner ?? throw new ArgumentNullException(nameof(planner)); + } + + /// + /// 按固定顺序创建地图并执行一次 Hybrid A* 粗路径规划。 + /// 参数:job 的地图输入使用 mm,位姿和车辆尺寸使用 m,航向使用 rad,曲率使用 1/m;cancellationToken 会传递给搜索阶段。 + /// 返回:始终同时保留地图创建结果和规划结果;地图创建失败时不会启动搜索,并返回 的空路径结果。 + /// + public CoarsePathPlanningJobResult Plan(CoarsePathPlanningJob job, + CancellationToken cancellationToken = default(CancellationToken)) + { + PlanningOperationBudget budget = CreateBudget(job, cancellationToken); + PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); + if (!mapResult.Succeeded || mapResult.Map == null) + { + PlanningResult mapFailure = PlanningResult.Failure(MapFailureStatus(mapResult), + new PlanningDiagnostics(elapsed: budget.Elapsed, terminationReason: BuildMapFailureReason(mapResult))); + return PublishDebug(job, mapResult, mapFailure); + } + + var request = new PlanningRequest + { + Map = mapResult.Map, + Start = job.Start, + Goal = job.Goal, + Vehicle = job.Vehicle, + Configuration = job.Configuration, + StartVehicleCurvature = job.StartVehicleCurvature, + StartDirection = job.StartDirection, + GoalDirection = job.GoalDirection, + }; + PlanningResult planningResult = _planner.Plan(request, budget); + return PublishDebug(job, mapResult, planningResult); + } + + private static PlanningOperationBudget CreateBudget(CoarsePathPlanningJob job, CancellationToken cancellationToken) + { + HybridAStarConfiguration configuration = job == null ? null : job.Configuration; + return configuration != null && configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, configuration.SearchTimeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + } + + private static PlanningStatus MapFailureStatus(PlanningMapBuildResult mapResult) + { + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) return PlanningStatus.Cancelled; + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) return PlanningStatus.SearchTimeout; + return PlanningStatus.InvalidMap; + } + + private static CoarsePathPlanningJobResult PublishDebug(CoarsePathPlanningJob job, PlanningMapBuildResult mapResult, + PlanningResult planningResult) + { + var diagnostics = new List(); + PlanningDebugOptions options = job == null ? null : job.DebugOptions; + if (options != null && options.Enabled) + { + IPlanningDebugSink sink = options.Sink ?? NullPlanningDebugSink.Instance; + try + { + sink.Publish(mapResult, planningResult); + } + catch (Exception exception) + { + diagnostics.Add("调试旁路发布失败:" + exception.GetType().Name + "。" + exception.Message); + } + } + + return new CoarsePathPlanningJobResult(mapResult, planningResult, diagnostics); + } + + private static string BuildMapFailureReason(PlanningMapBuildResult mapResult) + { + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) + return "规划地图创建已取消,未启动 Hybrid A* 搜索。"; + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) + return "规划地图创建已超时,未启动 Hybrid A* 搜索。"; + string reason = mapResult == null ? string.Empty : mapResult.FailureReason; + return string.IsNullOrEmpty(reason) ? "规划地图创建失败,未启动 Hybrid A* 搜索。" : + "规划地图创建失败,未启动 Hybrid A* 搜索:" + reason; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs new file mode 100644 index 0000000..3f6399e --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs @@ -0,0 +1,29 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 接收一次地图构建和粗路径规划完成后的旁路调试数据。 +/// 实现不得修改输入对象;服务会隔离实现抛出的异常,调试失败不会改变规划结果。 +/// +public interface IPlanningDebugSink +{ + /// + /// 发布本次编排得到的地图和规划结果。 + /// 参数:mapResult 为地图创建结果;planningResult 为对应的完整或失败规划结果;二者均不可由接收方修改。 + /// + void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult); +} + +internal sealed class NullPlanningDebugSink : IPlanningDebugSink +{ + internal static readonly NullPlanningDebugSink Instance = new NullPlanningDebugSink(); + + private NullPlanningDebugSink() + { + } + + public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) + { + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs new file mode 100644 index 0000000..a022f20 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs @@ -0,0 +1,23 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次粗路径编排的可选调试旁路配置。 +/// 调试开关和接收器不参与地图输入指纹、缓存键、搜索状态或路径结果。 +/// +public sealed class PlanningDebugOptions +{ + /// 创建默认关闭并使用空接收器的调试配置。 + public PlanningDebugOptions() + { + Enabled = false; + Sink = NullPlanningDebugSink.Instance; + } + + /// 是否在本次编排结束后向 发布旁路数据;默认值为 false。 + public bool Enabled { get; set; } + + /// + /// 接收地图和规划结果的旁路对象;默认为空实现。赋值为 null 时服务仍会使用空实现,且不会影响主流程。 + /// + public IPlanningDebugSink Sink { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs new file mode 100644 index 0000000..697a488 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs @@ -0,0 +1,264 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Output; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 已建 上 Hybrid A* 粗路径规划的下层门面。 +/// 本类不建图、不读取 UI 或传感器;只有路径经回溯、装配和最终连续复核后才发布成功结果。 +/// +public sealed class HybridAStarPlanner +{ + private readonly HybridAStarSearch _search; + private readonly PathBacktracker _backtracker; + private readonly CoarsePathAssembler _assembler; + private readonly CoarsePathValidator _validator; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认搜索、回溯、装配和最终复核组件的规划器。 + public HybridAStarPlanner() + : this(new HybridAStarSearch(), new PathBacktracker(), new CoarsePathAssembler(), new CoarsePathValidator(), + new FootprintCollisionChecker()) + { + } + + internal HybridAStarPlanner(HybridAStarSearch search, PathBacktracker backtracker, CoarsePathAssembler assembler, + CoarsePathValidator validator, FootprintCollisionChecker collisionChecker) + { + _search = search ?? throw new ArgumentNullException(nameof(search)); + _backtracker = backtracker ?? throw new ArgumentNullException(nameof(backtracker)); + _assembler = assembler ?? throw new ArgumentNullException(nameof(assembler)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 在请求提供的不可变规划地图上执行一次 Hybrid A* 粗路径规划。 + /// 参数:request 的位置单位为 m、航向单位为 rad、曲率单位为 1/m;cancellationToken 会在搜索扩展检查点取消。 + /// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 携带完整路径和分段。 + /// + public PlanningResult Plan(PlanningRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, request.Configuration.SearchTimeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + return Plan(request, budget); + } + + /// 使用门面传入的共享预算执行预检、搜索和最终路径复核。 + internal PlanningResult Plan(PlanningRequest request, PlanningOperationBudget budget) + { + Stopwatch pathSearchStopwatch = null; + try + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return Failure(ToPlanningStatus(stopReason), budget, "规划在开始前已停止。", null); + + PlanningStatus preflightStatus = ValidatePreflight(request, out string preflightReason); + if (preflightStatus != PlanningStatus.Success) + return Failure(preflightStatus, budget, preflightReason, null); + + if (!IsFootprintInsideMap(request.Start, request.Map, request.Vehicle)) + return Failure(PlanningStatus.StartOutsideMap, budget, "起始扩大车体不完全位于地图边界内。", null); + if (!_collisionChecker.IsPoseCollisionFree(request.Start, request.Map, request.Vehicle, 0d, out _)) + return Failure(PlanningStatus.StartInCollision, budget, "起始扩大车体与障碍物相交或擦边。", null); + if (!IsFootprintInsideMap(request.Goal, request.Map, request.Vehicle)) + return Failure(PlanningStatus.GoalOutsideMap, budget, "目标扩大车体不完全位于地图边界内。", null); + if (!_collisionChecker.IsPoseCollisionFree(request.Goal, request.Map, request.Vehicle, 0d, out _)) + return Failure(PlanningStatus.GoalInCollision, budget, "目标扩大车体与障碍物相交或擦边。", null); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return Failure(ToPlanningStatus(stopReason), budget, "规划在搜索前已停止。", null); + pathSearchStopwatch = Stopwatch.StartNew(); + HybridAStarSearchResult searchResult = _search.Search(request, budget); + if (searchResult == null) + return Failure(PlanningStatus.InternalError, budget, "搜索器未返回结果。", null, pathSearchStopwatch); + if (searchResult.Status != PlanningStatus.Success) + return Failure(searchResult.Status, budget, + BuildSearchFailureReason(searchResult, request.Configuration), searchResult, pathSearchStopwatch); + + if (!_backtracker.TryBacktrack(searchResult, request, out BacktrackedPath backtrackedPath, out string backtrackingReason)) + return Failure(PlanningStatus.BacktrackingFailed, budget, backtrackingReason, searchResult, pathSearchStopwatch); + if (!_assembler.TryAssemble(backtrackedPath, request, out var path, out var segments, out string assemblyReason)) + return Failure(PlanningStatus.FinalValidationFailed, budget, assemblyReason, searchResult, pathSearchStopwatch); + if (!_validator.TryValidate(path, segments, request, out double minimumClearanceMeters, out string validationReason)) + return Failure(PlanningStatus.FinalValidationFailed, budget, validationReason, searchResult, pathSearchStopwatch); + + double pathLengthMeters = path[path.Count - 1].ArcLength; + return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, + minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); + } + catch (Exception exception) + { + string reason = "规划内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return Failure(PlanningStatus.InternalError, budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), + reason, null, pathSearchStopwatch); + } + } + + private static PlanningStatus ValidatePreflight(PlanningRequest request, out string failureReason) + { + failureReason = string.Empty; + if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || + !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || + !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) + { + failureReason = "规划请求缺少必要对象或包含非法数值。"; + return PlanningStatus.InvalidRequest; + } + + PlanningGridMap map = request.Map; + if (map.Bounds == null || map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters)) + { + failureReason = "规划地图结构无效。"; + return PlanningStatus.InvalidMap; + } + if (!map.PlanningReady) + { + failureReason = string.IsNullOrEmpty(map.PlanningBlockReason) ? "规划地图尚未就绪。" : map.PlanningBlockReason; + return PlanningStatus.MapNotReady; + } + + VehicleParameters vehicle = request.Vehicle; + if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || + !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + { + failureReason = "车辆尺寸、安全余量或曲率限制无效。"; + return PlanningStatus.InvalidVehicleParameters; + } + + HybridAStarConfiguration configuration = request.Configuration; + if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || + (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) + { + failureReason = "Hybrid A* 曲率、离散、代价或资源配置无效。"; + return PlanningStatus.InvalidCurvatureConfiguration; + } + + return PlanningStatus.Success; + } + + private static bool IsValidConfiguration(HybridAStarConfiguration configuration) + { + return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && + NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && + NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && + NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && + configuration.HeadingResolutionRadians <= 2d * Math.PI && configuration.CurvatureLevelCount >= 3 && + configuration.CurvatureLevelCount % 2 == 1 && NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && + configuration.GoalPositionToleranceMeters >= 0d && NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && + configuration.GoalHeadingToleranceRadians >= 0d && configuration.MaximumExpandedNodes >= 0 && + configuration.SearchTimeout >= TimeSpan.Zero && NumericGuard.IsFinite(configuration.HeuristicWeight) && + configuration.HeuristicWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && + NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && + NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && + NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && + configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && NumericGuard.IsFinite(configuration.ClearanceCostWeight) && + configuration.ClearanceCostWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); + } + + private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) + { + if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; + + double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; + double longitudinalX = Math.Cos(pose.Heading); + double longitudinalY = Math.Sin(pose.Heading); + double lateralX = -longitudinalY; + double lateralY = longitudinalX; + for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) + for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) + { + double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; + double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + + return true; + } + + private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, + HybridAStarConfiguration configuration) + { + string reason = string.IsNullOrEmpty(searchResult.TerminationReason) + ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" + : searchResult.TerminationReason; + string resourceLimit = string.Empty; + if (searchResult.Status == PlanningStatus.SearchTimeout) + { + resourceLimit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( + "F3", CultureInfo.InvariantCulture) + "s;"; + } + else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) + { + resourceLimit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( + CultureInfo.InvariantCulture) + ";"; + } + + return reason + resourceLimit + + "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + + "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; + } + + private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, + HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) + { + TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; + return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, + reason, pathSearchElapsed)); + } + + private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, + double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) + { + return new PlanningDiagnostics( + searchResult == null ? 0 : searchResult.ExpandedNodeCount, + searchResult == null ? 0 : searchResult.GeneratedNodeCount, + searchResult == null ? 0 : searchResult.ReopenedNodeCount, + searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, + searchResult == null ? 0 : searchResult.PeakOpenListCount, + pathLengthMeters, + minimumClearanceMeters, + elapsed, + reason, + pathSearchElapsed); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) + { + if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; + if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs new file mode 100644 index 0000000..29df53d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs @@ -0,0 +1,216 @@ +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()); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs new file mode 100644 index 0000000..e9a12a3 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; + +/// +/// 对准备发布的粗路径执行独立的连续安全与输出契约复核。 +/// 复核失败的路径不得被包装为 。 +/// +public sealed class CoarsePathValidator +{ + private const double NumericTolerance = 1e-6d; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认连续车体检查器的最终路径复核器。 + public CoarsePathValidator() + : this(new FootprintCollisionChecker()) + { + } + + /// 创建使用指定连续车体检查器的最终路径复核器。 + public CoarsePathValidator(FootprintCollisionChecker collisionChecker) + { + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 复核路径数值、曲率、连续扫掠碰撞、终点约束、累计弧长和方向分段。 + /// 参数:path 与 segments 为待发布输出;request 必须是生成该路径的请求;minimumBodyClearanceMeters 返回沿途的保守净空下界。 + /// 返回:所有规则通过时为 true;否则返回 false、写入失败原因,调用方必须丢弃 path 和 segments。 + /// + public bool TryValidate(IReadOnlyList path, IReadOnlyList segments, + PlanningRequest request, out double minimumBodyClearanceMeters, out string failureReason) + { + minimumBodyClearanceMeters = 0d; + failureReason = string.Empty; + if (path == null || segments == null || request == null || request.Map == null || request.Vehicle == null || + request.Configuration == null || path.Count == 0 || segments.Count == 0) + { + failureReason = "最终路径或复核请求为空。"; + return false; + } + + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvaturePerMeter)) + { + failureReason = "车辆曲率约束无效。"; + return false; + } + + CoarsePathPoint first = path[0]; + if (!IsValidPoint(first) || first.Source != CoarsePathPointSource.Start || first.IsGearSwitchPoint || + Math.Abs(first.ArcLength) > NumericTolerance || !IsSamePose(first, request.Start)) + { + failureReason = "最终路径首点不符合起点契约。"; + return false; + } + + double minimumClearance = double.PositiveInfinity; + for (int index = 0; index < path.Count; index++) + { + CoarsePathPoint current = path[index]; + if (!IsValidPoint(current) || Math.Abs(current.VehicleCurvature) > maximumCurvaturePerMeter + NumericTolerance) + { + failureReason = "最终路径包含非法数值或超限曲率。"; + return false; + } + + var currentPose = new Pose2D(current.X, current.Y, current.Heading); + if (!_collisionChecker.IsPoseCollisionFree(currentPose, request.Map, request.Vehicle, 0d, out double poseClearanceMeters) || + IsClearanceOverclaimed(current.BodyClearance, poseClearanceMeters)) + { + failureReason = "最终路径点未通过连续车体碰撞复核。"; + return false; + } + + minimumClearance = Math.Min(minimumClearance, current.BodyClearance); + if (index == 0) continue; + + CoarsePathPoint previous = path[index - 1]; + if (current.ArcLength + NumericTolerance < previous.ArcLength || + !IsUnwrappedHeadingContinuous(previous, current)) + { + failureReason = "最终路径的弧长或展开航向不连续。"; + return false; + } + + bool isDuplicate = IsDuplicatePoseAndArcLength(previous, current); + if (isDuplicate) + { + if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) + { + failureReason = "相邻重复点不是合法换向对。"; + return false; + } + } + else + { + if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + NumericTolerance || + !IsArcIncrementConsistent(previous, current)) + { + failureReason = "非换向路径点的弧长增量不一致。"; + return false; + } + } + + var previousPose = new Pose2D(previous.X, previous.Y, previous.Heading); + if (!_collisionChecker.IsSweptMotionCollisionFree(previousPose, currentPose, request.Map, request.Vehicle, + request.Configuration.MaximumCollisionCheckStepMeters, out double sweptClearanceMeters)) + { + failureReason = "最终路径相邻点之间的车体扫掠碰撞复核失败。"; + return false; + } + + minimumClearance = Math.Min(minimumClearance, sweptClearanceMeters); + } + + CoarsePathPoint last = path[path.Count - 1]; + if (!GoalToleranceChecker.IsSatisfied(new Pose2D(last.X, last.Y, last.Heading), request.Goal, request.Configuration, + last.Direction, request.GoalDirection) || (path.Count > 1 && last.Source != CoarsePathPointSource.GoalTruncation)) + { + failureReason = "最终路径末点未满足终点容差、方向或来源契约。"; + return false; + } + + if (!AreSegmentsValid(path, segments, out failureReason)) return false; + minimumBodyClearanceMeters = minimumClearance; + return true; + } + + private static bool AreSegmentsValid(IReadOnlyList path, IReadOnlyList segments, out string failureReason) + { + failureReason = string.Empty; + int expectedStartIndex = 0; + for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) + { + PathSegment segment = segments[segmentIndex]; + if (segment == null || segment.SegmentIndex != segmentIndex || segment.StartIndex != expectedStartIndex || + segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count || + segment.StartsAtGearSwitch != path[segment.StartIndex].IsGearSwitchPoint) + { + failureReason = "方向分段索引或起始换向标记无效。"; + return false; + } + + for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) + { + if (path[pointIndex].Direction != segment.Direction) + { + failureReason = "方向分段包含不同方向的路径点。"; + return false; + } + } + + bool hasNextSegment = segmentIndex + 1 < segments.Count; + bool expectedEndsAtGearSwitch = hasNextSegment && segment.EndIndex + 1 < path.Count && + path[segment.EndIndex + 1].IsGearSwitchPoint; + if (segment.EndsAtGearSwitch != expectedEndsAtGearSwitch) + { + failureReason = "方向分段末尾换向标记无效。"; + return false; + } + + expectedStartIndex = segment.EndIndex + 1; + } + + if (expectedStartIndex != path.Count) + { + failureReason = "方向分段未完整覆盖路径点。"; + return false; + } + + return true; + } + + private static bool IsValidPoint(CoarsePathPoint point) + { + if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || + !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.UnwrappedHeading) || + !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || + !NumericGuard.IsFinite(point.VehicleCurvature) || !IsTravelDirection(point.Direction) || + !IsValidClearance(point.BodyClearance)) + return false; + + return Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= NumericTolerance; + } + + private static bool IsSamePose(CoarsePathPoint point, Pose2D pose) + { + return point != null && pose != null && Math.Abs(point.X - pose.X) <= NumericTolerance && + Math.Abs(point.Y - pose.Y) <= NumericTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, pose.Heading)) <= NumericTolerance; + } + + private static bool IsUnwrappedHeadingContinuous(CoarsePathPoint previous, CoarsePathPoint current) + { + double expectedDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + return NumericGuard.IsFinite(expectedDelta) && + Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedDelta) <= NumericTolerance; + } + + private static bool IsDuplicatePoseAndArcLength(CoarsePathPoint previous, CoarsePathPoint current) + { + return Math.Abs(previous.X - current.X) <= NumericTolerance && Math.Abs(previous.Y - current.Y) <= NumericTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= NumericTolerance && + Math.Abs(previous.ArcLength - current.ArcLength) <= NumericTolerance; + } + + private static bool IsArcIncrementConsistent(CoarsePathPoint previous, CoarsePathPoint current) + { + double expectedIncrement; + if (Math.Abs(current.VehicleCurvature) < 1e-12d) + { + double deltaX = current.X - previous.X; + double deltaY = current.Y - previous.Y; + expectedIncrement = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + } + else + { + double headingDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + expectedIncrement = Math.Abs(headingDelta / current.VehicleCurvature); + } + + return NumericGuard.IsFinite(expectedIncrement) && expectedIncrement > 0d && + Math.Abs((current.ArcLength - previous.ArcLength) - expectedIncrement) <= NumericTolerance; + } + + private static bool IsClearanceOverclaimed(double reportedClearanceMeters, double checkedClearanceMeters) + { + if (double.IsPositiveInfinity(reportedClearanceMeters)) return !double.IsPositiveInfinity(checkedClearanceMeters); + return reportedClearanceMeters > checkedClearanceMeters + NumericTolerance; + } + + private static bool IsValidClearance(double clearanceMeters) + { + return !double.IsNaN(clearanceMeters) && clearanceMeters >= 0d; + } + + private static bool IsTravelDirection(TravelDirection direction) + { + return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs new file mode 100644 index 0000000..37a55e6 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; + +/// +/// 成功搜索父链重新生成后的原语序列。 +/// 起点位置使用 m/rad,起始曲率使用 1/m;原语集合按从起点到终点的顺序排列。 +/// +public sealed class BacktrackedPath +{ + internal BacktrackedPath(Pose2D start, TravelDirection startDirection, double startCurvaturePerMeter, + IReadOnlyList primitives) + { + Start = start; + StartDirection = startDirection; + StartCurvaturePerMeter = startCurvaturePerMeter; + Primitives = primitives; + } + + /// 原始请求中的起始车辆中心位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; } + + /// 成功父链根节点记录的起步方向。 + public TravelDirection StartDirection { get; } + + /// 成功父链根节点离散后的起始曲率,单位 1/m。 + public double StartCurvaturePerMeter { get; } + + /// 按起点到终点顺序重新积分的恒曲率原语;每项都不包含自身起点。 + public IReadOnlyList Primitives { get; } +} + +/// +/// 仅依据成功节点的父索引和原语描述,确定性地重建稠密原语序列。 +/// 不复用搜索期保存的积分点,以保证输出与当前解析积分、扫掠检查规则一致。 +/// +public sealed class PathBacktracker +{ + private const double PoseComparisonTolerance = 1e-7d; + private const double LengthComparisonTolerance = 1e-7d; + private readonly MotionPrimitiveGenerator _primitiveGenerator; + + /// 创建使用默认解析积分器的回溯器。 + public PathBacktracker() + : this(new MotionPrimitiveGenerator()) + { + } + + /// 创建使用指定原语生成器的回溯器。 + public PathBacktracker(MotionPrimitiveGenerator primitiveGenerator) + { + _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); + } + + /// + /// 依据搜索成功节点重建从起点到终点的原语序列。 + /// 参数:searchResult 必须是携带成功节点索引的搜索结果;request 必须仍指向执行搜索的同一不可变地图和配置。 + /// 返回:父链无环、索引连续且每条原语按同一解析规则可重建时为 true;失败时 path 为 null 并返回可读原因。 + /// + public bool TryBacktrack(HybridAStarSearchResult searchResult, PlanningRequest request, + out BacktrackedPath path, out string failureReason) + { + path = null; + failureReason = string.Empty; + if (searchResult == null || request == null || searchResult.Status != PlanningStatus.Success || + !searchResult.SuccessNodeIndex.HasValue || searchResult.Nodes == null) + { + failureReason = "搜索结果不含可回溯的成功节点。"; + return false; + } + + int currentIndex = searchResult.SuccessNodeIndex.Value; + var reverseNodes = new List(); + var visited = new HashSet(); + while (currentIndex >= 0) + { + if (currentIndex >= searchResult.Nodes.Count || !visited.Add(currentIndex)) + { + failureReason = "搜索父链索引越界或存在环。"; + return false; + } + + HybridAStarNode current = searchResult.Nodes[currentIndex]; + if (current == null || current.NodeIndex != currentIndex || current.Pose == null) + { + failureReason = "搜索节点索引或连续位姿不一致。"; + return false; + } + + reverseNodes.Add(current); + currentIndex = current.ParentNodeIndex; + } + + if (reverseNodes.Count == 0) + { + failureReason = "搜索父链为空。"; + return false; + } + + reverseNodes.Reverse(); + HybridAStarNode root = reverseNodes[0]; + if (root.ParentNodeIndex != -1 || root.IncomingPrimitive != null || !IsFinitePose(root.Pose)) + { + failureReason = "搜索父链根节点无效。"; + return false; + } + + var primitives = new List(Math.Max(0, reverseNodes.Count - 1)); + Pose2D previousPose = root.Pose; + for (int index = 1; index < reverseNodes.Count; index++) + { + HybridAStarNode node = reverseNodes[index]; + MotionPrimitive descriptor = node.IncomingPrimitive; + if (node.ParentNodeIndex != reverseNodes[index - 1].NodeIndex || descriptor == null || + !IsFinitePose(node.Pose) || !NumericGuard.IsFinite(descriptor.CurvaturePerMeter) || + !IsTravelDirection(descriptor.Direction) || descriptor.ActualLengthMeters <= 0d) + { + failureReason = "搜索父链中的原语描述无效。"; + return false; + } + + // 只把恒曲率、方向和长度描述作为真源,再走一遍相同的解析积分与连续碰撞检查。 + MotionPrimitive rebuilt = _primitiveGenerator.Generate(previousPose, descriptor.CurvaturePerMeter, descriptor.Direction, request); + if (rebuilt == null || !IsEquivalent(descriptor, rebuilt) || !IsSamePose(rebuilt.End, node.Pose)) + { + failureReason = "搜索原语无法按当前解析规则确定性重建。"; + return false; + } + + primitives.Add(rebuilt); + previousPose = rebuilt.End; + } + + path = new BacktrackedPath(root.Pose, root.Direction, root.CurvaturePerMeter, + new ReadOnlyCollection(primitives)); + return true; + } + + private static bool IsEquivalent(MotionPrimitive expected, MotionPrimitive actual) + { + return expected.Direction == actual.Direction && expected.IsGoalTruncation == actual.IsGoalTruncation && + Math.Abs(expected.CurvaturePerMeter - actual.CurvaturePerMeter) <= PoseComparisonTolerance && + Math.Abs(expected.ActualLengthMeters - actual.ActualLengthMeters) <= LengthComparisonTolerance; + } + + private static bool IsSamePose(Pose2D first, Pose2D second) + { + return IsFinitePose(first) && IsFinitePose(second) && + Math.Abs(first.X - second.X) <= PoseComparisonTolerance && + Math.Abs(first.Y - second.Y) <= PoseComparisonTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(first.Heading, second.Heading)) <= PoseComparisonTolerance; + } + + 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; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md b/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md new file mode 100644 index 0000000..730a335 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/README.md @@ -0,0 +1,387 @@ +# CoarsePath 粗路径规划(P0/P1) + +`CoarsePath` 在 `Map` 提供的不可变 `PlanningGridMap` 上执行 Hybrid A*,输出已经过连续碰撞、终点和输出不变量复核的粗路径。它只处理“能否安全地从当前车辆几何中心到达目标”的几何搜索;不读取传感器或定位,不绘制 UI,也不向底盘发送任何命令。 + +地图障碍物来源、世界坐标栅格化、距离场和缓存细节由 [Map/README.md](../Map/README.md) 说明。本模块唯一建议的业务调用入口是: + +```csharp +CoarsePathPlanningService.Plan(job, cancellationToken) +``` + +## 模块说明(Module Overview) + +| 模块 | 负责内容 | 不负责内容 | +| --- | --- | --- | +| `Map` | 外部障碍物快照、占据栅格、保守障碍距离、地图缓存 | 车辆足迹、运动原语、路径搜索、控制 | +| `CoarsePath` | 车辆扩大足迹、连续碰撞检查、前进/倒车原语、Hybrid A*、路径复核与方向分段 | 传感器读取、定位读取、速度规划、路径跟踪、底盘命令 | +| `Facade` | 将建图、搜索、取消、总预算和可选调试旁路编排为一次调用 | 修改地图内容、写入 AMR 占据、执行轨迹 | +| `Test` | 固定回归场景、P1 手动测试入口与规划结果可视化 | 真实作业地图、实时重规划或车辆控制 | + +安全余量只由 `VehicleParameters.SafetyMarginMeters` 扩大车辆矩形。它不会回写到地图障碍物,因此同一个 `PlanningGridMap` 可由不同车辆参数重复使用。 + +## 文件结构(File Structure) + +```text +CoarsePath/ +├── README.md # 本模块说明:结构、数据流、调用与测试 +├── HybridAStarPlanner.cs # 公开规划门面后的核心编排:校验、搜索、回溯和复核 +├── Contracts/ +│ ├── Pose2D.cs # 车辆几何中心位姿:m / rad +│ ├── PlanningRequest.cs # 已有 PlanningGridMap 上的一次内部搜索请求 +│ ├── PlanningResult.cs # 不可变规划结果:成功路径或空结果 +│ ├── PlanningStatus.cs # 成功、取消、无解、输入和资源限制状态 +│ ├── CoarsePathPoint.cs # 稠密路径点、方向、曲率、净空和换向标记 +│ ├── PathSegment.cs # 前进或倒车的包含式路径索引段 +│ ├── VehicleParameters.cs # 车体尺寸、安全余量和曲率限制 +│ ├── HybridAStarConfiguration.cs # 原语、离散、代价、容差和资源上限 +│ └── GoalDirectionConstraint.cs # 目标进入方向约束 +├── Vehicle/ +│ ├── VehicleKinematics.cs # 恒曲率车辆运动学积分 +│ ├── VehicleFootprint.cs # 扩大后的车辆矩形几何 +│ ├── OrientedRectangleCellIntersection.cs # 旋转矩形与占据格相交判定 +│ └── FootprintCollisionChecker.cs # 连续扫掠的足迹碰撞检查 +├── Search/ +│ ├── BinaryMinHeap.cs # 可更新优先级的 Open List +│ ├── GridDijkstraHeuristic.cs # 二维栅格可达性和距离启发式 +│ ├── GoalToleranceChecker.cs # 目标位置、航向和方向约束判定 +│ ├── MotionPrimitive.cs # 单个前进或倒车恒曲率原语 +│ ├── MotionPrimitiveGenerator.cs # 原语离散与连续积分点生成 +│ ├── SearchCostCalculator.cs # 长度、倒车、换向、曲率和净空代价 +│ ├── HybridAStarNode.cs # 搜索节点与父链信息 +│ ├── HybridAStarNodeKey.cs # 离散状态键 +│ └── HybridAStarSearch.cs # Hybrid A* 主搜索循环 +├── Output/ +│ ├── PathBacktracker.cs # 从终点节点安全回溯父链 +│ ├── CoarsePathAssembler.cs # 组装稠密路径和方向段 +│ └── CoarsePathValidator.cs # 对最终输出重新进行连续复核 +├── Facade/ +│ ├── CoarsePathPlanningJob.cs # 一次完整业务输入:地图请求、位姿、车辆、配置 +│ ├── CoarsePathPlanningJobResult.cs # 同时包含地图结果和规划结果的不可变输出 +│ ├── CoarsePathPlanningService.cs # 唯一业务调用门面 +│ ├── PlanningDebugOptions.cs # 可选调试旁路配置 +│ └── IPlanningDebugSink.cs # 调试旁路接收器契约 +└── Test/ + ├── CoarsePathScenarioFactory.cs # 六个固定场景和手动目标演示请求工厂 + └── MovementTest.CoarsePathTest.cs # 七个 Clumsy 入口、后台取消和 Painter 绘制 +``` + +## 规划数据流(Planning Data Flow) + +```text +CoarsePathPlanningJob + │ MapRequest 使用 mm;Pose2D/车辆使用 m、rad + ▼ +CoarsePathPlanningService.Plan(job, cancellationToken) + │ + ├── PlanningMapFactory.Create(job.MapRequest) + │ │ + │ ├── 失败、取消或超时 + │ │ └── MapResult + 空路径 PlanningResult,搜索不启动 + │ │ + │ └── 成功:不可变 PlanningGridMap + ▼ +PlanningRequest + │ + ▼ +HybridAStarPlanner + ├── 车辆扩大足迹与连续碰撞检查 + ├── GridDijkstraHeuristic + Hybrid A* 搜索 + ├── PathBacktracker + CoarsePathAssembler + └── CoarsePathValidator 最终复核 + ▼ +PlanningResult + MapResult + ▼ +CoarsePathPlanningJobResult +``` + +调用方只创建 `CoarsePathPlanningJob` 并消费 `CoarsePathPlanningJobResult`。`PlanningRequest`、`HybridAStarPlanner`、原语和碰撞检查器属于模块内部协作对象,不应由 UI、传感器或 MovementTest 直接拼接。 + +## 构建状态与停止(Build Status and Stop) + +必须一起处理 `MapResult` 和 `PlanningResult`。`MapResult.Status` 的类型是 `PlanningMapBuildStatus`;地图失败时,门面返回对应的空路径结果,并且不会启动 Hybrid A*。 + +| 情况 | `MapResult` | `PlanningResult` | 调用方处理 | +| --- | --- | --- | --- | +| 地图和搜索成功 | `Success` 且 `Map` 非空 | `Success`,发布完整路径和方向段 | 消费粗路径;后续模块仍需自行进行平滑、速度和控制 | +| 地图输入/来源失败 | `Failed` | `InvalidMap` | 读取 `FailureReason`,修复地图输入 | +| 调用被取消 | `Cancelled` | `Cancelled` | 不重试为普通无解;不会发布地图或部分路径 | +| 总预算耗尽 | `TimedOut` | `SearchTimeout` | 根据上层策略调整预算或稍后重试 | +| 搜索无解 | 地图成功 | `NoFeasiblePath` | 当前地图、车体和运动约束下无可行路径 | +| 节点或搜索资源受限 | 地图成功 | `SearchNodeLimitExceeded` 或 `SearchTimeout` | 读取诊断后调整配置或上层策略 | + +除 `PlanningStatus.Success` 外,`PlanningResult.Path` 与 `PlanningResult.Segments` 始终为空。取消、超时、无解、输入错误和最终复核失败都不能作为“部分可执行路径”使用。 + +### 总预算与取消 + +`HybridAStarConfiguration.SearchTimeout` 是从门面开始计时的一次总预算,依次覆盖建图、距离场、二维启发式和 Hybrid A*。同一个 `CancellationToken` 会沿调用链传递,取消优先于超时。 + +### 总耗时与路径搜索耗时 + +`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed`(路径搜索耗时)从地图和起终点预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核;搜索开始前失败时为零。 + +## 坐标与单位(Coordinates and Units) + +| 数据 | 单位 | 说明 | +| --- | --- | --- | +| `PlanningMapRequest.Bounds`、分辨率、障碍物几何 | mm | 来自 Map 的世界坐标;边界采用 `[min, max)` | +| `Pose2D.X`、`Pose2D.Y`、路径位置、车辆尺寸、安全余量、弧长 | m | CoarsePath 的连续世界坐标和长度 | +| `Pose2D.Heading`、航向容差 | rad | 核心一律使用弧度 | +| 曲率、起步曲率 | 1/m | 最大曲率或最小转弯半径至少提供一个 | +| `PlanningGridMap` 世界查询参数 | m | 越界位置按占据处理,净距为 0 | +| P1 的 AMR/手动目标 X/Y | mm | 仅在 UI 边界读取,进入核心前除以 1000 | +| P1 的 AMR/手动目标航向 | deg | 仅在 UI 边界转换为 `deg * PI / 180 -> rad` | + +起点和终点都表示车辆**几何中心**。若上游定位参考点是雷达、天线或其他安装点,必须先在上游应用安装外参;不要在 CoarsePath 内猜测偏移。车辆外扩由 `VehicleParameters.SafetyMarginMeters` 表达,不要把余量写入 Map 障碍物。 + +## 最小调用示例(Minimal Call Example) + +以下示例明确允许空图,因而只适合算法或单位演示。真实作业必须通过 `IMapObstacleSource` 提供有效障碍物快照;如何构造来源请阅读 [Map/README.md](../Map/README.md)。 + +```csharp +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.Mapping; + +var service = new CoarsePathPlanningService(); // 长期持有,保留地图缓存 + +var job = new CoarsePathPlanningJob +{ + MapRequest = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = Array.Empty(), + AllowExplicitEmptyMap = true, // 仅演示时明确允许 + }, + Start = new Pose2D(1d, 1d, 0d), + Goal = new Pose2D(3d, 1d, 0d), + Vehicle = new VehicleParameters + { + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, + }, + Configuration = new HybridAStarConfiguration(), + GoalDirection = GoalDirectionConstraint.Forward, +}; + +CoarsePathPlanningJobResult result = + service.Plan(job, CancellationToken.None); + +if (!result.MapResult.Succeeded) + throw new InvalidOperationException(result.MapResult.FailureReason); + +if (result.PlanningResult.Status != PlanningStatus.Success) + throw new InvalidOperationException( + result.PlanningResult.Diagnostics.TerminationReason); + +foreach (CoarsePathPoint point in result.PlanningResult.Path) + Console.WriteLine(point.X + "," + point.Y + "," + point.Heading); +``` + +## 缓存与 SourceVersion(Cache and SourceVersion) + +`CoarsePathPlanningService` 在生命周期内长期持有 `PlanningMapFactory`,因此重复调用时能够复用地图缓存。不要每次规划都新建服务,否则会失去缓存收益。 + +| 缓存层级 | 条件 | 结果 | +| --- | --- | --- | +| `Input` | 边界、分辨率、空图策略、来源 ID、`SourceVersion`、必需性和来源结果相同 | 返回同一个不可变 `PlanningGridMap` | +| `Occupancy` | 输入版本变化,但最终占据栅格相同 | 复用占据/距离数组,生成新的快照元数据 | +| `None` | 占据内容变化 | 重建规划快照和距离场 | + +障碍来源内容改变时,调用方必须递增该来源的 `SourceVersion`。仅修改起终点、车辆、搜索配置、调试开关或调试接收器不会改变地图输入;改变障碍物却不递增版本则可能错误复用旧快照。 + +## 详细使用指南(Detailed Usage Guide) + +本节说明调用方如何从一个地图输入得到可消费的粗路径。所有业务调用都通过 `CoarsePathPlanningService.Plan(job, cancellationToken)` 完成。 + +### 第 1 步:长期持有服务 + +服务持有地图工厂和规划器,应该作为规划业务、任务执行器或上层服务的长期字段,而不是在每次调用中创建: + +```csharp +private readonly CoarsePathPlanningService _coarsePathService = + new CoarsePathPlanningService(); +``` + +### 第 2 步:准备地图请求 + +创建 `PlanningMapRequest`,其边界、分辨率和障碍物仍使用 mm。使用手工圆形/矩形或 TwoLeg 快照时,应先按 [Map/README.md](../Map/README.md) 将它们包装为 `IMapObstacleSource`,并为内容变化递增 `SourceVersion`。 + +```csharp +var mapRequest = new PlanningMapRequest +{ + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = sources, + AllowExplicitEmptyMap = false, +}; +``` + +`AllowExplicitEmptyMap = true` 只在调用方明确确认空地图安全时使用。未提供有效障碍物且未显式允许空图时,地图不会进入规划。 + +### 第 3 步:填写起点、终点和方向约束 + +将车辆几何中心的世界 X/Y 从 mm 转为 m,并将航向转换为 rad 后创建 `Pose2D`。`StartDirection = null` 表示允许从前进或倒车开始;`GoalDirection` 可以限制最终进入目标的方向。 + +```csharp +var start = new Pose2D(startXmm / 1000d, startYmm / 1000d, + startHeadingDeg * Math.PI / 180d); +var goal = new Pose2D(goalXmm / 1000d, goalYmm / 1000d, + goalHeadingDeg * Math.PI / 180d); +``` + +### 第 4 步:填写车辆参数 + +车辆尺寸和安全余量全部为 m。曲率限制可填写 `MaximumCurvaturePerMeter`,或填写 `MinimumTurningRadiusMeters`;至少必须提供一个有效限制。 + +```csharp +var vehicle = new VehicleParameters +{ + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, +}; +``` + +### 第 5 步:调整搜索配置 + +默认 `HybridAStarConfiguration` 包含原语长度、积分步长、航向离散、终点容差、代价、节点上限和总超时。若业务需要覆盖默认值,应同时理解安全影响:`MaximumCollisionCheckStepMeters` 不能以牺牲连续碰撞检查精度为代价随意增大。 + +```csharp +var configuration = new HybridAStarConfiguration +{ + SearchTimeout = TimeSpan.FromSeconds(5d), + MaximumExpandedNodes = 200000, +}; +``` + +### 第 6 步:调用并消费成功结果 + +只有 `Success` 可以发布完整路径。`Path` 是稠密点序列;`Segments` 是覆盖整条路径的前进/倒车包含式索引段,可供后续的速度规划或显示模块消费。 + +```csharp +var job = new CoarsePathPlanningJob +{ + MapRequest = mapRequest, + Start = start, + Goal = goal, + Vehicle = vehicle, + Configuration = configuration, + GoalDirection = GoalDirectionConstraint.Any, +}; + +CoarsePathPlanningJobResult result = _coarsePathService.Plan(job, cancellationToken); +if (!result.MapResult.Succeeded) + ReportMapFailure(result.MapResult.FailureReason); +else if (result.PlanningResult.Status == PlanningStatus.Success) + ConsumeCoarsePath(result.PlanningResult.Path, result.PlanningResult.Segments); +else + ReportPlanningFailure(result.PlanningResult.Diagnostics.TerminationReason); +``` + +`CoarsePathPoint.IsGearSwitchPoint` 为 `true` 表示该点是新方向段开始处。换向位置会保留一对位置、航向与弧长相同、方向不同的相邻点;`UnwrappedHeading` 用于跨越 `-pi/pi` 时保持显示连续。 + +## P1 手动测试与可视化(P1 Manual Tests and Visualization) + +P1 在 `Test/MovementTest.CoarsePathTest.cs` 提供只读测试入口。它们共用一个长期存活的 `CoarsePathPlanningService`,只提交规划并绘制结果;不发送底盘、速度或转向命令。 + +| MovementTest 名称 | 场景 | 预期 | +| --- | --- | --- | +| `粗路径规划-显式空图` | 显式允许的空图 | 前进直达成功 | +| `粗路径规划-单矩形绕行` | 中央矩形阻断直线 | 成功绕障 | +| `粗路径规划-多来源障碍` | 手工圆形、矩形和 TwoLeg 快照 | 证明多来源经过同一门面 | +| `粗路径规划-缓存命中` | 重复相同地图输入 | 后续调用显示 `Input` 缓存命中 | +| `粗路径规划-倒车换向` | 前进起步、倒车到达 | 成功路径含 `IsGearSwitchPoint` | +| `粗路径规划-无解` | 贯穿地图的障碍带 | 返回 `NoFeasiblePath` 且不发布路径 | +| `粗路径规划` | 当前 AMR 位姿、人工终点和可选人工障碍物 | 验证手动障碍物、边界、路径与可视化 | + +### 固定案例的实时 AMR 锚定 + +`粗路径规划-显式空图`、`粗路径规划-单矩形绕行`、`粗路径规划-多来源障碍`、`粗路径规划-缓存命中`、`粗路径规划-倒车换向` 和 `粗路径规划-无解` 是六个固定案例。它们不再以写死的世界起点运行:共享运行器在前台仅调用一次 `DetourInterface.getCartLocation()`,校验并冻结本次 AMR 的世界 `X(mm)`、`Y(mm)` 与航向 `deg`,再创建本次规划请求;后台规划期间不会再次读取定位。 + +`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)` + +该入口把基准案例的起点映射为冻结的 AMR 位姿,并以相同的 `ΔX/ΔY` 平移地图边界、目标、圆形/矩形障碍,以及 TwoLeg 的检测原点。因此,固定案例始终在当前 AMR 附近保留原有的相对几何关系。起点航向严格使用冻结的 AMR 航向;终点航向保持基准案例的“终点航向减起点航向”差值,叠加到当前 AMR 航向后规范化到 `[-pi, pi]`。TwoLeg 只平移检测原点,`DetectionHeadingRadians` 不会因 AMR 航向发生旋转。 + +`粗路径规划-缓存命中` 只有两次运行冻结到相同的 AMR `X/Y`、从而形成相同的平移后地图输入时,才作为缓存命中场景;AMR 位置移动后,地图输入正常未命中并重建快照。仅 AMR 航向变化不会改变固定案例的地图输入,地图缓存仍可命中。若定位读取为空、抛出异常,或 `X`、`Y`、航向含有 `NaN`/无穷值,运行器不会提交后台规划,状态与 Toast 会显示以“AMR 位姿不可用”开头的诊断原因。 + +手动 `粗路径规划` 的人工终点、障碍物和超时输入流程保持不变;它不套用固定案例的整体平移规则。 + +### AMR 位姿、手动终点与障碍物 + +`CoarsePathPlanningTest` 启动时读取一次 `DetourInterface.getCartLocation()`,将当前 AMR 世界位姿冻结为起点;随后依次输入目标世界 `X(mm)`、`Y(mm)`、航向 `deg`,以及障碍物数量 `0-20`。每个障碍物再依次输入类型和几何参数: + +| 类型输入 | 形状 | 输入参数(全部为 mm) | +| --- | --- | --- | +| `1` | 圆形 | 几何中心 `X`、`Y` 与半径;半径必须大于 0 | +| `2` | 轴对齐矩形 | 几何中心 `X`、`Y`、X 向长度、Y 向宽度;两个尺寸必须大于 0 | + +矩形只支持 `AxisAlignedRectangle`,不提供旋转角;其中心和长宽由 `ManualCoarsePathObstacle.AxisAlignedRectangle` 表达。圆形由 `ManualCoarsePathObstacle.Circle` 表达。所有无穷、NaN、非数字或不合法尺寸都会在输入阶段拒绝。 + +`CoarsePathScenarioFactory.CreateManualObstacleDemo` 在唯一边界完成转换: + +```text +AMR/目标 X、Y:mm / 1000 -> m +AMR/目标航向:deg * PI / 180 -> rad +``` + +当数量为 0 时,入口使用显式空图;`CreateManualGoalDemo` 保留为同一零障碍物场景的兼容帮助方法。数量大于 0 时,工厂将 `ManualCoarsePathObstacle` 快照封装成来源 ID 为 `manual-user-input` 的地图输入,并为每次手动快照分配新的 `SourceVersion`,避免错误复用地图缓存。规划边界覆盖起点、终点和每个障碍物的完整轮廓,再增加 8000 mm 余量并按 50 mm 对齐。 + +手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。 + +此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。 + +`getCartLocation` 在无有效定位时可能阻塞,因此应在定位准备完成的测试环境使用。手动障碍物是测试输入,不能替代现场障碍物来源;零障碍物的显式空图也绝不代表现场不存在障碍物。 + +### 后台执行、停止与图层 + +每次测试启动时会创建独立的 `CancellationTokenSource`,以 `Task.Run` 调用门面,并先取消旧会话。`Test()` 不等待任务,也不读取 `Task.Result`;`TestStop` 取消当前令牌、使会话失效并清空图层。已取消任务完成后不会覆盖新会话,也不会显示部分路径。 + +专用世界坐标 Painter 图层为 `CoarsePathPlanningV1`。它直接读取本次 `PlanningGridMap` 的 `Bounds`、`ResolutionMm`、`SnapshotId` 和 `IsOccupied(row, col)`,因此边界、抽稀网格和占据格与实际规划快照一致,而不是重新绘制原始障碍物。 + +状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加 `TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。 + +| 颜色 | 可视化元素 | +| --- | --- | +| 灰白 | 地图边界与栅格网络 | +| 暗红 | 占据格 | +| 绿色 | 起点、前进路径和方向箭头 | +| 橙色 | 终点、航向和位置容差圈 | +| 天蓝 | 倒车路径和方向箭头 | +| 紫色 | 换向点 | +| 金色 | 已纳入安全余量的车辆检查框 | + +自动化已检查 UI 入口的后台、取消和数据来源结构。仍需在实际 Clumsy 界面手动运行“粗路径规划-单矩形绕行”和“粗路径规划”,确认图层交互显示与停止按钮效果。 + +## 常见错误(Common Errors) + +| 现象 | 原因 | 处理 | +| --- | --- | --- | +| 起点、终点或障碍物位置相差 1000 倍 | 将 mm 直接传给 `Pose2D` 或把 m 传给地图输入 | Map 输入使用 mm;`Pose2D`、车辆和路径使用 m | +| 路径朝向错误或旋转异常 | 将 P1 的 deg 直接当作核心 rad | 在 UI/上层边界执行 `deg * PI / 180`,核心只保存 rad | +| 障碍物已经变化却复用旧地图 | 内容变更后没有递增 `SourceVersion` | 每次来源快照内容变化后增加对应版本号 | +| 地图创建成功但规划被阻止 | 没有有效障碍物且未显式允许空图 | 提供有效来源;仅在确认安全的演示中设置 `AllowExplicitEmptyMap = true` | +| 无解、取消或超时后仍尝试使用路径 | 没有检查 `PlanningStatus.Success` | 仅成功时消费 `Path` 和 `Segments`;其他状态读取诊断 | +| 将粗路径直接下发给车辆 | 粗路径不包含速度、时间、执行控制或实时安全闭环 | 在后续阶段增加平滑、时间参数化、跟踪和独立安全控制 | +| P1 手动终点表现为空场地安全 | 手动入口使用显式空图演示 | 真实作业必须提供真实障碍物快照,不能复用空图语义 | + +## 第一版限制(First-Version Limits) + +当前 P0/P1 已提供安全、确定性的粗路径核心和手动结果可视化,但不包含: + +- 路径平滑或曲率连续优化; +- Reeds-Shepp 或 Dubins 精确终点连接; +- 速度、加速度、时间标注、时间轨迹和路径跟踪控制; +- 底盘命令、避障闭环、现场传感器采集或实时重规划调度; +- 横移、蟹行或其他非前进/倒车运动原语; +- 原地旋转; +- 真实作业地图接入、交互式场景编辑和 Release 性能/资源/确定性基准。 + +当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。 + +因此,调用方只能把 `Success` 结果视作后续模块的粗路径输入,不能把它当作可直接下发的时间轨迹。 diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs new file mode 100644 index 0000000..5e2f369 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 为 Hybrid A* Open List 提供确定性优先级的二叉最小堆。 +/// 排序严格依次比较 F、H、较大的 G 和插入序号;F、H、G 必须为有限且非负的等效米代价。 +/// +/// 与一组搜索代价关联的节点或条目类型。 +public sealed class BinaryMinHeap +{ + private readonly List _entries = new List(); + private long _nextInsertionSequence; + + /// 创建空的确定性 Open List 堆。 + public BinaryMinHeap() + { + } + + /// 当前堆内尚未出队的条目数量。 + public int Count { get { return _entries.Count; } } + + /// + /// 将一个条目和其搜索排序代价压入堆。 + /// 参数:item 为关联条目;f、h、g 均为有限且非负的等效米代价。 + /// 失败:任一代价无效、item 为 null(仅引用类型)或插入序号耗尽时抛出异常。 + /// + public void Push(T item, double f, double h, double g) + { + if (ReferenceEquals(item, null)) throw new ArgumentNullException(nameof(item)); + ValidateCost(f, nameof(f)); + ValidateCost(h, nameof(h)); + ValidateCost(g, nameof(g)); + if (_nextInsertionSequence == long.MaxValue) + throw new InvalidOperationException("The binary heap insertion sequence has been exhausted."); + + var entry = new HeapEntry(item, f, h, g, _nextInsertionSequence); + _nextInsertionSequence++; + _entries.Add(entry); + SiftUp(_entries.Count - 1); + } + + /// + /// 弹出当前排序最优的条目。 + /// 返回:按 F、H、较大 G 和插入序号排序后的最小条目;空堆时抛出 。 + /// + public T Pop() + { + if (_entries.Count == 0) throw new InvalidOperationException("The binary heap is empty."); + + HeapEntry result = _entries[0]; + int lastIndex = _entries.Count - 1; + if (lastIndex == 0) + { + _entries.RemoveAt(0); + return result.Item; + } + + _entries[0] = _entries[lastIndex]; + _entries.RemoveAt(lastIndex); + SiftDown(0); + return result.Item; + } + + /// 清空尚未出队的条目;后续插入序号继续单调递增以保持整个实例内的确定性。 + public void Clear() + { + _entries.Clear(); + } + + private void SiftUp(int index) + { + while (index > 0) + { + int parentIndex = (index - 1) / 2; + if (Compare(_entries[index], _entries[parentIndex]) >= 0) return; + Swap(index, parentIndex); + index = parentIndex; + } + } + + private void SiftDown(int index) + { + while (true) + { + int leftChildIndex = index * 2 + 1; + if (leftChildIndex >= _entries.Count) return; + + int bestChildIndex = leftChildIndex; + int rightChildIndex = leftChildIndex + 1; + if (rightChildIndex < _entries.Count && Compare(_entries[rightChildIndex], _entries[leftChildIndex]) < 0) + bestChildIndex = rightChildIndex; + + if (Compare(_entries[bestChildIndex], _entries[index]) >= 0) return; + Swap(index, bestChildIndex); + index = bestChildIndex; + } + } + + private void Swap(int firstIndex, int secondIndex) + { + HeapEntry temporary = _entries[firstIndex]; + _entries[firstIndex] = _entries[secondIndex]; + _entries[secondIndex] = temporary; + } + + private static int Compare(HeapEntry left, HeapEntry right) + { + int comparison = left.F.CompareTo(right.F); + if (comparison != 0) return comparison; + + comparison = left.H.CompareTo(right.H); + if (comparison != 0) return comparison; + + comparison = right.G.CompareTo(left.G); + if (comparison != 0) return comparison; + + return left.InsertionSequence.CompareTo(right.InsertionSequence); + } + + private static void ValidateCost(double value, string parameterName) + { + if (!NumericGuard.IsFinite(value) || value < 0d) + throw new ArgumentOutOfRangeException(parameterName, "Search costs must be finite and non-negative."); + } + + private sealed class HeapEntry + { + public HeapEntry(T item, double f, double h, double g, long insertionSequence) + { + Item = item; + F = f; + H = h; + G = g; + InsertionSequence = insertionSequence; + } + + public T Item { get; } + public double F { get; } + public double H { get; } + public double G { get; } + public long InsertionSequence { get; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs new file mode 100644 index 0000000..3d18a04 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs @@ -0,0 +1,61 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// 按位置、航向和进入方向约束判定连续位姿是否可以作为目标候选。 +public static class GoalToleranceChecker +{ + /// + /// 判断一个位姿是否满足目标容差和进入方向约束。 + /// 参数:pose 与 goal 使用世界 m/rad;configuration 提供位置 m 和航向 rad 容差;direction 为候选末段方向;goalDirection 为目标进入方向约束。 + /// 返回:输入有限、位置距离和最小环形航向误差均不超过容差且方向匹配时为 true;无效输入保守地返回 false。 + /// + public static bool IsSatisfied( + Pose2D pose, + Pose2D goal, + HybridAStarConfiguration configuration, + TravelDirection direction, + GoalDirectionConstraint goalDirection) + { + if (!IsFinitePose(pose) || !IsFinitePose(goal) || configuration == null || + !NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) || + !NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) || + configuration.GoalPositionToleranceMeters < 0d || configuration.GoalHeadingToleranceRadians < 0d || + !IsTravelDirection(direction) || !IsGoalDirection(goalDirection)) + return false; + + if (!MatchesDirection(direction, goalDirection)) return false; + + double deltaX = pose.X - goal.X; + double deltaY = pose.Y - goal.Y; + double positionDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + if (!NumericGuard.IsFinite(positionDistanceMeters) || positionDistanceMeters > configuration.GoalPositionToleranceMeters) + return false; + + double headingDifferenceRadians = Math.Abs(AngleMath.ShortestSignedDifference(pose.Heading, goal.Heading)); + return NumericGuard.IsFinite(headingDifferenceRadians) && headingDifferenceRadians <= configuration.GoalHeadingToleranceRadians; + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private static bool MatchesDirection(TravelDirection direction, GoalDirectionConstraint constraint) + { + return constraint == GoalDirectionConstraint.Any || + (constraint == GoalDirectionConstraint.Forward && direction == TravelDirection.Forward) || + (constraint == GoalDirectionConstraint.Reverse && direction == TravelDirection.Reverse); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs new file mode 100644 index 0000000..509bf8a --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 基于不可变栅格地图的目标反向八邻域 Dijkstra 启发式。 +/// 距离单位为 m;对角移动仅在两个对应正交邻格都未占据时允许,以避免从障碍夹角穿越。 +/// +public sealed class GridDijkstraHeuristic +{ + private readonly PlanningGridMap _map; + private readonly double[] _costs; + + /// + /// 从目标栅格预计算所有可达自由格到目标的二维最短距离。 + /// 参数:map 必须是已就绪的不可变地图;goalRow、goalCol 为地图内且未占据的目标格索引。 + /// 失败:地图为空、未就绪或目标格无效时抛出异常。 + /// + public GridDijkstraHeuristic(PlanningGridMap map, int goalRow, int goalCol) + { + if (!TryCreate(map, goalRow, goalCol, PlanningOperationBudget.Unlimited(CancellationToken.None), + out GridDijkstraHeuristic heuristic, out _)) + throw new InvalidOperationException("Unbounded Dijkstra construction unexpectedly stopped."); + _map = heuristic._map; + _costs = heuristic._costs; + } + + private GridDijkstraHeuristic(PlanningGridMap map, double[] costs) + { + _map = map; + _costs = costs; + } + + /// 使用共享预算创建完整二维启发式;停止时不返回部分成本数组。 + internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, PlanningOperationBudget budget, + out GridDijkstraHeuristic heuristic, out PlanningOperationStopReason stopReason) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + if (!map.PlanningReady) throw new ArgumentException("The planning map must be ready.", nameof(map)); + if (goalRow < 0 || goalRow >= map.Rows || goalCol < 0 || goalCol >= map.Cols) + throw new ArgumentOutOfRangeException(nameof(goalRow)); + if (map.IsOccupied(goalRow, goalCol)) + throw new ArgumentException("The goal grid cell must be free.", nameof(goalRow)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + + heuristic = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + var costs = new double[checked(map.Rows * map.Cols)]; + int workItemCount = 0; + for (int index = 0; index < costs.Length; index++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + costs[index] = double.PositiveInfinity; + } + if (!TryBuild(map, costs, goalRow, goalCol, budget, ref workItemCount, out stopReason)) return false; + heuristic = new GridDijkstraHeuristic(map, costs); + stopReason = PlanningOperationStopReason.None; + return true; + } + + /// + /// 查询指定栅格到构造时目标格的二维最短距离。 + /// 参数:row、col 为从零开始的栅格索引。 + /// 返回:单位 m 的有限最短距离;自由格不可达或索引越界时返回正无穷。 + /// + public double GetCost(int row, int col) + { + return row < 0 || row >= _map.Rows || col < 0 || col >= _map.Cols + ? double.PositiveInfinity + : _costs[row * _map.Cols + col]; + } + + private static bool TryBuild(PlanningGridMap map, double[] costs, int goalRow, int goalCol, + PlanningOperationBudget budget, ref int workItemCount, out PlanningOperationStopReason stopReason) + { + var openList = new BinaryMinHeap(); + int goalIndex = goalRow * map.Cols + goalCol; + costs[goalIndex] = 0d; + openList.Push(goalIndex, 0d, 0d, 0d); + + while (openList.Count > 0) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + int currentIndex = openList.Pop(); + int currentRow = currentIndex / map.Cols; + int currentCol = currentIndex % map.Cols; + double currentCost = costs[currentIndex]; + + for (int rowOffset = -1; rowOffset <= 1; rowOffset++) + for (int colOffset = -1; colOffset <= 1; colOffset++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + if (rowOffset == 0 && colOffset == 0) continue; + + int nextRow = currentRow + rowOffset; + int nextCol = currentCol + colOffset; + if (nextRow < 0 || nextRow >= map.Rows || nextCol < 0 || nextCol >= map.Cols || map.IsOccupied(nextRow, nextCol)) + continue; + + bool isDiagonal = rowOffset != 0 && colOffset != 0; + if (isDiagonal && (map.IsOccupied(currentRow + rowOffset, currentCol) || map.IsOccupied(currentRow, currentCol + colOffset))) + continue; + + double stepCost = isDiagonal ? Math.Sqrt(2d) * map.ResolutionMeters : map.ResolutionMeters; + double candidateCost = currentCost + stepCost; + int nextIndex = nextRow * map.Cols + nextCol; + if (candidateCost >= costs[nextIndex]) continue; + + costs[nextIndex] = candidateCost; + openList.Push(nextIndex, candidateCost, 0d, 0d); + } + } + stopReason = PlanningOperationStopReason.None; + return true; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs new file mode 100644 index 0000000..b3ce03d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs @@ -0,0 +1,99 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// Hybrid A* 运行期节点。 +/// 节点保留连续位姿和其离散闭集键;搜索过程中不会修改已创建节点,改进代价时会追加新节点并使旧 Open List 条目失效。 +/// +public sealed class HybridAStarNode +{ + /// + /// 创建一个搜索节点。 + /// 参数:nodeIndex 为本次搜索内稳定索引;key 为离散状态;pose 为连续车辆中心位姿;parentNodeIndex 为父节点索引,根节点使用 -1; + /// incomingPrimitive 为父节点到本节点的原语,根节点为 null;curvaturePerMeter、gCostMeters 和 hCostMeters 均使用规划器的标准单位。 + /// + public HybridAStarNode( + int nodeIndex, + HybridAStarNodeKey key, + Pose2D pose, + int parentNodeIndex, + MotionPrimitive incomingPrimitive, + double curvaturePerMeter, + double gCostMeters, + double hCostMeters) + : this(nodeIndex, key, pose, parentNodeIndex, incomingPrimitive, curvaturePerMeter, gCostMeters, hCostMeters, false) + { + } + + /// + /// 创建一个搜索节点,并显式指定它是否为原语内部命中的终点候选。 + /// 参数:isGoalCandidate 为 true 时,本节点不参与普通离散状态的最优 G 值支配;它仍必须在从 Open List 出队后重新通过连续终点和碰撞检查才能成功。 + /// + public HybridAStarNode( + int nodeIndex, + HybridAStarNodeKey key, + Pose2D pose, + int parentNodeIndex, + MotionPrimitive incomingPrimitive, + double curvaturePerMeter, + double gCostMeters, + double hCostMeters, + bool isGoalCandidate) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (pose == null) throw new ArgumentNullException(nameof(pose)); + + NodeIndex = nodeIndex; + Key = key; + Pose = pose; + ParentNodeIndex = parentNodeIndex; + IncomingPrimitive = incomingPrimitive; + CurvaturePerMeter = curvaturePerMeter; + GCostMeters = gCostMeters; + HCostMeters = hCostMeters; + IsGoalCandidate = isGoalCandidate; + } + + /// 本次搜索节点数组中的稳定索引。 + public int NodeIndex { get; } + + /// 本节点用于 Open/Closed 状态管理的离散键。 + public HybridAStarNodeKey Key { get; } + + /// 未量化的连续车辆中心位姿。 + public Pose2D Pose { get; } + + /// 父节点稳定索引;根节点为 -1。 + public int ParentNodeIndex { get; } + + /// 由父节点驶入本节点的已连续碰撞检查原语;根节点为 null。 + public MotionPrimitive IncomingPrimitive { get; } + + /// 含义相同的原语别名。 + public MotionPrimitive Primitive { get { return IncomingPrimitive; } } + + /// 当前末段采用的车辆曲率,单位 1/m。 + public double CurvaturePerMeter { get; } + + /// 当前节点的累计等效米代价。 + public double GCostMeters { get; } + + /// 当前节点的二维启发式等效米代价。 + public double HCostMeters { get; } + + /// + /// 本节点是否由原语内部首次满足终点条件而生成。 + /// 终点候选必须保留连续位姿,不能因同一离散键的普通低代价节点而被 Open List 准入规则压制。 + /// + public bool IsGoalCandidate { get; } + + /// Open List 排序使用的总等效米代价。 + public double FCostMeters { get { return GCostMeters + HCostMeters; } } + + /// 本节点末段的行驶方向。 + public TravelDirection Direction { get { return Key.Direction; } } + + /// 本节点末段的曲率等级索引。 + public int CurvatureLevelIndex { get { return Key.CurvatureLevelIndex; } } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs new file mode 100644 index 0000000..e75342c --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs @@ -0,0 +1,67 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// Hybrid A* 闭集使用的离散状态键。 +/// 位置使用地图行列索引;航向、行驶方向和曲率等级共同保留车辆运动学状态,避免把同一栅格中的不同可达姿态错误合并。 +/// +public sealed class HybridAStarNodeKey : IEquatable +{ + /// + /// 创建一个离散 Hybrid A* 状态键。 + /// 参数:row、column 为零开始的地图行列;headingIndex 为航向桶;direction 为末段行驶方向;curvatureLevelIndex 为末段曲率等级。 + /// + public HybridAStarNodeKey(int row, int column, int headingIndex, TravelDirection direction, int curvatureLevelIndex) + { + Row = row; + Column = column; + HeadingIndex = headingIndex; + Direction = direction; + CurvatureLevelIndex = curvatureLevelIndex; + } + + /// 车辆中心所在的零开始地图行索引。 + public int Row { get; } + + /// 车辆中心所在的零开始地图列索引。 + public int Column { get; } + + /// 含义相同的列索引别名。 + public int Col { get { return Column; } } + + /// 根据配置航向分辨率量化后的航向桶索引。 + public int HeadingIndex { get; } + + /// 到达当前节点的最后一段行驶方向。 + public TravelDirection Direction { get; } + + /// 到达当前节点的最后一段曲率等级索引。 + public int CurvatureLevelIndex { get; } + + /// 判断另一个键是否表示完全相同的离散搜索状态。 + public bool Equals(HybridAStarNodeKey other) + { + return other != null && Row == other.Row && Column == other.Column && HeadingIndex == other.HeadingIndex && + Direction == other.Direction && CurvatureLevelIndex == other.CurvatureLevelIndex; + } + + /// 判断另一个对象是否表示完全相同的离散搜索状态。 + public override bool Equals(object obj) + { + return Equals(obj as HybridAStarNodeKey); + } + + /// 返回用于闭集字典的稳定哈希值。 + public override int GetHashCode() + { + unchecked + { + int hashCode = Row; + hashCode = hashCode * 397 ^ Column; + hashCode = hashCode * 397 ^ HeadingIndex; + hashCode = hashCode * 397 ^ (int)Direction; + return hashCode * 397 ^ CurvatureLevelIndex; + } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs new file mode 100644 index 0000000..cf05967 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs @@ -0,0 +1,542 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 单次 Hybrid A* 搜索的只读结果。 +/// 即使失败也会保留已经创建的运行期节点,供上层记录诊断;仅 有值。 +/// +public sealed class HybridAStarSearchResult +{ + internal HybridAStarSearchResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason) + { + Status = status; + Nodes = new ReadOnlyCollection(new List(nodes ?? Array.Empty())); + ExpandedNodeCount = expandedNodeCount; + GeneratedNodeCount = generatedNodeCount; + ReopenedNodeCount = reopenedNodeCount; + StaleOpenListEntryCount = staleOpenListEntryCount; + PeakOpenListCount = peakOpenListCount; + SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null; + TerminationReason = status == PlanningStatus.Success ? string.Empty : terminationReason ?? string.Empty; + } + + /// 搜索终止状态。 + public PlanningStatus Status { get; } + + /// 本次搜索已经创建的全部节点;索引与 一致。 + public IReadOnlyList Nodes { get; } + + /// 实际从 Open List 弹出并扩展的节点数量。 + public int ExpandedNodeCount { get; } + + /// 已进入 Open List 的节点数量,包含根节点和因改进代价追加的节点。 + public int GeneratedNodeCount { get; } + + /// 更优路径到达已关闭离散状态、并重新放回 Open List 的次数。 + public int ReopenedNodeCount { get; } + + /// 从 Open List 弹出后因已有更优普通状态而被丢弃的陈旧条目数量。 + public int StaleOpenListEntryCount { get; } + + /// 搜索期间 Open List 持有的最大条目数,包含等待惰性丢弃的旧条目。 + public int PeakOpenListCount { get; } + + /// 成功时最后一个从 Open List 弹出且满足终点条件的节点索引;失败时为 null。 + public int? SuccessNodeIndex { get; } + + /// 搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。 + public string TerminationReason { get; } +} + +/// +/// 在不可变 上执行前进/倒车恒曲率原语的 Hybrid A* 搜索。 +/// 搜索只消费已准备好的地图快照;所有候选原语均先完成连续扩大车体碰撞检查,再参与 Open List 排序。 +/// +public sealed class HybridAStarSearch +{ + private const double CostImprovementToleranceMeters = 1e-9d; + private readonly MotionPrimitiveGenerator _primitiveGenerator; + private readonly SearchCostCalculator _costCalculator; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认原语、代价和碰撞检查实现的搜索器。 + public HybridAStarSearch() + : this(new MotionPrimitiveGenerator(), new SearchCostCalculator(), new FootprintCollisionChecker()) + { + } + + /// 创建使用指定协作对象的搜索器,便于在不引入地图或 UI 依赖的情况下测试搜索过程。 + public HybridAStarSearch( + MotionPrimitiveGenerator primitiveGenerator, + SearchCostCalculator costCalculator, + FootprintCollisionChecker collisionChecker) + { + _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); + _costCalculator = costCalculator ?? throw new ArgumentNullException(nameof(costCalculator)); + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 执行一次不可变地图上的 Hybrid A* 搜索。 + /// 参数:request 提供地图、起终点、车辆和搜索配置;cancellationToken 在每次节点扩展前检查。 + /// 返回:成功仅在满足目标约束的候选已从 Open List 弹出时报告;任一失败状态均不报告成功节点。 + /// + public HybridAStarSearchResult Search(PlanningRequest request, CancellationToken cancellationToken) + { + TimeSpan timeout = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? request.Configuration.SearchTimeout + : TimeSpan.Zero; + PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, timeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + return Search(request, budget); + } + + /// 使用门面传入的共享预算执行搜索;预算从整次规划开始计时。 + internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget) + { + var nodes = new List(); + int expandedNodeCount = 0; + int generatedNodeCount = 0; + int reopenedNodeCount = 0; + int staleOpenListEntryCount = 0; + int peakOpenListCount = 0; + + try + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + PlanningStatus validationStatus = ValidateRequest(request); + if (validationStatus != PlanningStatus.Success) + return CreateResult(validationStatus, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + PlanningGridMap map = request.Map; + HybridAStarConfiguration configuration = request.Configuration; + VehicleParameters vehicle = request.Vehicle; + + if (!IsFootprintInsideMap(request.Start, map, vehicle)) + return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!_collisionChecker.IsPoseCollisionFree(request.Start, map, vehicle, 0d, out _)) + return CreateResult(PlanningStatus.StartInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!IsFootprintInsideMap(request.Goal, map, vehicle)) + return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!_collisionChecker.IsPoseCollisionFree(request.Goal, map, vehicle, 0d, out _)) + return CreateResult(PlanningStatus.GoalInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (configuration.MaximumExpandedNodes == 0) + return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + if (!map.TryWorldToGrid(request.Goal.X, request.Goal.Y, out int goalRow, out int goalColumn)) + return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!GridDijkstraHeuristic.TryCreate(map, goalRow, goalColumn, budget, out GridDijkstraHeuristic heuristic, out stopReason)) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return CreateResult(PlanningStatus.InvalidVehicleParameters, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + IReadOnlyList curvatureLevels = _primitiveGenerator.GetCurvatureLevels(vehicle, configuration); + if (curvatureLevels.Count == 0) + return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + int headingBinCount = GetHeadingBinCount(configuration.HeadingResolutionRadians); + int startCurvatureLevelIndex = GetNearestCurvatureLevelIndex(curvatureLevels, request.StartVehicleCurvature); + if (startCurvatureLevelIndex < 0) + return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + if (!map.TryWorldToGrid(request.Start.X, request.Start.Y, out int startRow, out int startColumn)) + return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + var openList = new BinaryMinHeap(); + var bestStates = new Dictionary(); + foreach (TravelDirection startDirection in GetStartDirections(request, configuration)) + { + int headingIndex = AngleMath.ToHeadingIndex(request.Start.Heading, configuration.HeadingResolutionRadians, headingBinCount); + var key = new HybridAStarNodeKey(startRow, startColumn, headingIndex, startDirection, startCurvatureLevelIndex); + if (bestStates.ContainsKey(key)) continue; + + double hCostMeters = GetHeuristicCost(heuristic, startRow, startColumn, configuration); + if (double.IsPositiveInfinity(hCostMeters)) continue; + var node = new HybridAStarNode(nodes.Count, key, request.Start, -1, null, + curvatureLevels[startCurvatureLevelIndex], 0d, hCostMeters); + nodes.Add(node); + bestStates.Add(key, new NodeLabel(node.NodeIndex, 0d, false)); + openList.Push(node.NodeIndex, node.FCostMeters, node.HCostMeters, node.GCostMeters); + peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); + generatedNodeCount++; + } + + if (openList.Count == 0) + return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, + "二维启发式标记起点不可达目标,或起始方向无法进入 Open List。"); + + while (openList.Count > 0) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (expandedNodeCount >= configuration.MaximumExpandedNodes) + return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + int nodeIndex = openList.Pop(); + HybridAStarNode current = nodes[nodeIndex]; + NodeLabel currentLabel = null; + if (!current.IsGoalCandidate) + { + if (!bestStates.TryGetValue(current.Key, out currentLabel) || currentLabel.NodeIndex != nodeIndex || currentLabel.IsClosed) + { + staleOpenListEntryCount++; + continue; + } + + currentLabel.IsClosed = true; + } + + expandedNodeCount++; + if (current.IsGoalCandidate) + { + if (!GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection) || + !_collisionChecker.IsPoseCollisionFree(current.Pose, map, vehicle, 0d, out _)) + continue; + + return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); + } + + if (GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection)) + return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); + + for (int curvatureLevelIndex = 0; curvatureLevelIndex < curvatureLevels.Count; curvatureLevelIndex++) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!MotionPrimitiveGenerator.AreCurvatureLevelsAdjacent(current.CurvatureLevelIndex, curvatureLevelIndex)) continue; + + foreach (TravelDirection direction in GetSuccessorDirections(configuration)) + { + MotionPrimitive primitive = _primitiveGenerator.Generate(current.Pose, curvatureLevels[curvatureLevelIndex], direction, request); + if (primitive == null || primitive.ActualLengthMeters <= 0d) continue; + + if (!map.TryWorldToGrid(primitive.End.X, primitive.End.Y, out int row, out int column)) continue; + double hCostMeters = GetHeuristicCost(heuristic, row, column, configuration); + if (double.IsPositiveInfinity(hCostMeters)) continue; + + double bodyClearanceMeters = GetMinimumBodyClearance(primitive); + bool isGearSwitch = current.ParentNodeIndex >= 0 && current.Direction != direction; + int curvatureLevelDelta = curvatureLevelIndex - current.CurvatureLevelIndex; + double incrementalCostMeters = _costCalculator.Calculate( + primitive.ActualLengthMeters, + direction, + isGearSwitch, + curvatureLevels[curvatureLevelIndex], + maximumCurvaturePerMeter, + curvatureLevelDelta, + bodyClearanceMeters, + configuration); + double gCostMeters = current.GCostMeters + incrementalCostMeters; + if (!NumericGuard.IsFinite(gCostMeters) || gCostMeters < 0d) continue; + + int headingIndex = AngleMath.ToHeadingIndex(primitive.End.Heading, configuration.HeadingResolutionRadians, headingBinCount); + var key = new HybridAStarNodeKey(row, column, headingIndex, direction, curvatureLevelIndex); + bestStates.TryGetValue(key, out NodeLabel existingLabel); + var successor = new HybridAStarNode(nodes.Count, key, primitive.End, current.NodeIndex, primitive, + curvatureLevels[curvatureLevelIndex], gCostMeters, hCostMeters, primitive.IsGoalTruncation); + if (existingLabel != null && !ShouldEnqueueSuccessor(successor, existingLabel.GCostMeters)) continue; + + if (!successor.IsGoalCandidate && existingLabel != null && existingLabel.IsClosed) reopenedNodeCount++; + nodes.Add(successor); + if (!successor.IsGoalCandidate) + bestStates[key] = new NodeLabel(successor.NodeIndex, gCostMeters, false); + openList.Push(successor.NodeIndex, successor.FCostMeters, successor.HCostMeters, successor.GCostMeters); + peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); + generatedNodeCount++; + } + } + } + + return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, + "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"); + } + catch (Exception exception) + { + string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, reason); + } + } + + private static HybridAStarSearchResult CreateResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason = null) + { + string reason = status == PlanningStatus.Success + ? string.Empty + : terminationReason ?? GetDefaultTerminationReason(status); + return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason); + } + + private static string GetDefaultTerminationReason(PlanningStatus status) + { + switch (status) + { + case PlanningStatus.Cancelled: + return "Hybrid A* 搜索已取消。"; + case PlanningStatus.InvalidRequest: + return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。"; + case PlanningStatus.InvalidMap: + return "Hybrid A* 搜索地图结构无效。"; + case PlanningStatus.MapNotReady: + return "Hybrid A* 搜索地图尚未准备好。"; + case PlanningStatus.InvalidVehicleParameters: + return "Hybrid A* 搜索车辆参数无效。"; + case PlanningStatus.InvalidCurvatureConfiguration: + return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。"; + case PlanningStatus.StartOutsideMap: + return "Hybrid A* 搜索起始扩大车体不完全位于地图内。"; + case PlanningStatus.StartInCollision: + return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。"; + case PlanningStatus.GoalOutsideMap: + return "Hybrid A* 搜索目标扩大车体不完全位于地图内。"; + case PlanningStatus.GoalInCollision: + return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。"; + case PlanningStatus.SearchTimeout: + return "Hybrid A* 搜索使用的总规划预算已耗尽。"; + case PlanningStatus.SearchNodeLimitExceeded: + return "Hybrid A* 搜索达到扩展节点上限。"; + case PlanningStatus.NoFeasiblePath: + return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"; + case PlanningStatus.BacktrackingFailed: + return "Hybrid A* 成功节点无法回溯为完整父链。"; + case PlanningStatus.FinalValidationFailed: + return "Hybrid A* 路径未通过最终复核。"; + case PlanningStatus.InternalError: + return "Hybrid A* 搜索发生未预期内部错误。"; + default: + return "Hybrid A* 搜索以未识别状态终止:" + status + "。"; + } + } + + private static PlanningStatus ValidateRequest(PlanningRequest request) + { + if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || + !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || + !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) + return PlanningStatus.InvalidRequest; + + PlanningGridMap map = request.Map; + if (map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || map.Bounds == null) + return PlanningStatus.InvalidMap; + if (!map.PlanningReady) return PlanningStatus.MapNotReady; + + VehicleParameters vehicle = request.Vehicle; + if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || + !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return PlanningStatus.InvalidVehicleParameters; + + HybridAStarConfiguration configuration = request.Configuration; + if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || + (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) + return PlanningStatus.InvalidCurvatureConfiguration; + + return PlanningStatus.Success; + } + + private static bool IsValidConfiguration(HybridAStarConfiguration configuration) + { + return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && + NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && + NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && + NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && + configuration.HeadingResolutionRadians <= 2d * Math.PI && + configuration.CurvatureLevelCount >= 3 && configuration.CurvatureLevelCount % 2 == 1 && + NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && configuration.GoalPositionToleranceMeters >= 0d && + NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && configuration.GoalHeadingToleranceRadians >= 0d && + configuration.MaximumExpandedNodes >= 0 && configuration.SearchTimeout >= TimeSpan.Zero && + NumericGuard.IsFinite(configuration.HeuristicWeight) && configuration.HeuristicWeight >= 0d && + NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && + NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && + NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && + NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && + NumericGuard.IsFinite(configuration.ClearanceCostWeight) && configuration.ClearanceCostWeight >= 0d && + NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); + } + + private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) + { + if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; + + double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; + double longitudinalX = Math.Cos(pose.Heading); + double longitudinalY = Math.Sin(pose.Heading); + double lateralX = -longitudinalY; + double lateralY = longitudinalX; + for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) + for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) + { + double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; + double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + return true; + } + + private static IEnumerable GetStartDirections(PlanningRequest request, HybridAStarConfiguration configuration) + { + if (request.StartDirection.HasValue) + { + yield return request.StartDirection.Value; + yield break; + } + + yield return TravelDirection.Forward; + if (configuration.AllowReverse) yield return TravelDirection.Reverse; + } + + private static IEnumerable GetSuccessorDirections(HybridAStarConfiguration configuration) + { + yield return TravelDirection.Forward; + if (configuration.AllowReverse) yield return TravelDirection.Reverse; + } + + private static int GetHeadingBinCount(double headingResolutionRadians) + { + double rawBinCount = Math.Ceiling(2d * Math.PI / headingResolutionRadians); + if (!NumericGuard.IsFinite(rawBinCount) || rawBinCount < 1d || rawBinCount > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(headingResolutionRadians)); + return (int)rawBinCount; + } + + private static int GetNearestCurvatureLevelIndex(IReadOnlyList curvatureLevels, double curvaturePerMeter) + { + int bestIndex = -1; + double smallestDifference = double.PositiveInfinity; + for (int index = 0; index < curvatureLevels.Count; index++) + { + double difference = Math.Abs(curvatureLevels[index] - curvaturePerMeter); + if (difference >= smallestDifference) continue; + smallestDifference = difference; + bestIndex = index; + } + return bestIndex; + } + + private static double GetMinimumBodyClearance(MotionPrimitive primitive) + { + double minimum = double.PositiveInfinity; + foreach (double clearanceMeters in primitive.BodyClearancesMeters) + { + if (double.IsNaN(clearanceMeters) || clearanceMeters < 0d) return 0d; + minimum = Math.Min(minimum, clearanceMeters); + } + return minimum; + } + + private static double GetHeuristicCost(GridDijkstraHeuristic heuristic, int row, int column, HybridAStarConfiguration configuration) + { + double gridCostMeters = heuristic.GetCost(row, column); + if (double.IsPositiveInfinity(gridCostMeters)) return gridCostMeters; + double weightedCostMeters = gridCostMeters * configuration.HeuristicWeight; + return NumericGuard.IsFinite(weightedCostMeters) && weightedCostMeters >= 0d + ? weightedCostMeters + : double.PositiveInfinity; + } + + /// + /// 判断后继是否应进入 Open List。 + /// 参数:successor 是已完成连续碰撞检查的后继;bestKnownGCostMeters 是相同离散键普通状态的当前最优 G 值。 + /// 返回:普通状态仅在严格改善最优 G 值时入队;终点候选始终入队,以保留其未量化的连续终点位姿。 + /// + private static bool ShouldEnqueueSuccessor(HybridAStarNode successor, double bestKnownGCostMeters) + { + if (successor == null || !NumericGuard.IsFinite(bestKnownGCostMeters)) return false; + return successor.IsGoalCandidate || successor.GCostMeters < bestKnownGCostMeters - CostImprovementToleranceMeters; + } + + private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) + { + if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; + if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private sealed class NodeLabel + { + public NodeLabel(int nodeIndex, double gCostMeters, bool isClosed) + { + NodeIndex = nodeIndex; + GCostMeters = gCostMeters; + IsClosed = isClosed; + } + + public int NodeIndex { get; } + public double GCostMeters { get; } + public bool IsClosed { get; set; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs new file mode 100644 index 0000000..fba41ed --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 一条经连续碰撞检查的恒曲率运动原语。 +/// 位置和长度单位为 m,航向单位为 rad,曲率单位为 1/m; 不包含起点,只包含按积分顺序产生的后续点。 +/// +public sealed class MotionPrimitive +{ + /// + /// 创建不可变运动原语。 + /// 参数:start 为原语起点;direction 为行驶方向;curvaturePerMeter 为恒定曲率;actualLengthMeters 为已实际行驶长度; + /// points 与 bodyClearancesMeters 按同一索引保存内部积分位姿和对应的车体净空;isGoalTruncation 表示末点是否首次命中目标容差。 + /// + public MotionPrimitive( + Pose2D start, + TravelDirection direction, + double curvaturePerMeter, + double actualLengthMeters, + IEnumerable points, + IEnumerable bodyClearancesMeters, + bool isGoalTruncation) + { + if (start == null) throw new ArgumentNullException(nameof(start)); + if (points == null) throw new ArgumentNullException(nameof(points)); + if (bodyClearancesMeters == null) throw new ArgumentNullException(nameof(bodyClearancesMeters)); + + var copiedPoints = new List(points); + var copiedClearances = new List(bodyClearancesMeters); + if (copiedPoints.Count != copiedClearances.Count) + throw new ArgumentException("The point and clearance counts must match.", nameof(bodyClearancesMeters)); + + Start = start; + Direction = direction; + CurvaturePerMeter = curvaturePerMeter; + ActualLengthMeters = actualLengthMeters; + Points = new ReadOnlyCollection(copiedPoints); + BodyClearancesMeters = new ReadOnlyCollection(copiedClearances); + End = copiedPoints.Count == 0 ? start : copiedPoints[copiedPoints.Count - 1]; + IsGoalTruncation = isGoalTruncation; + } + + /// 原语起始车辆几何中心位姿。 + public Pose2D Start { get; } + + /// 原语最后一个积分点;零长度终点候选时等于 + public Pose2D End { get; } + + /// 原语对应的行驶方向。 + public TravelDirection Direction { get; } + + /// 原语全程采用的恒定车辆曲率,单位 1/m。 + public double CurvaturePerMeter { get; } + + /// 的实际行驶弧长,单位 m。 + public double ActualLengthMeters { get; } + + /// 不含起点的连续积分位姿,只读且按行驶顺序排列。 + public IReadOnlyList Points { get; } + + /// 一一对应的扩大车体保守净空下界,单位 m。 + public IReadOnlyList BodyClearancesMeters { get; } + + /// 末点是否因首次满足目标位置、航向和方向约束而截断。 + public bool IsGoalTruncation { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs new file mode 100644 index 0000000..817cc58 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 生成经解析积分和连续碰撞检查的前进或倒车恒曲率原语。 +/// 每个积分点均依次经过有限数值检查、与前一点之间的扫掠碰撞检查和终点容差检查。 +/// +public sealed class MotionPrimitiveGenerator +{ + private const double StraightCurvatureThreshold = 1e-12d; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认车辆连续碰撞检查器的原语生成器。 + public MotionPrimitiveGenerator() + : this(new FootprintCollisionChecker()) + { + } + + /// 创建使用指定连续车辆碰撞检查器的原语生成器。 + public MotionPrimitiveGenerator(FootprintCollisionChecker collisionChecker) + { + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 使用完整规划请求生成一条原语。 + /// 参数:start 为当前连续位姿;curvaturePerMeter 为候选恒定曲率;direction 为前进或倒车;request 提供地图、车辆、配置和目标。 + /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;否则返回最大长度不超过配置上限的原语。 + /// + public MotionPrimitive Generate(Pose2D start, double curvaturePerMeter, TravelDirection direction, PlanningRequest request) + { + if (request == null) return null; + return Generate(start, curvaturePerMeter, direction, request.Map, request.Vehicle, request.Configuration, request.Goal, request.GoalDirection); + } + + /// + /// 使用显式地图、车辆、配置和目标生成一条原语。 + /// 参数:所有位置使用 m/rad,curvaturePerMeter 使用 1/m;goalDirection 限制末段允许的进入方向。 + /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;首次命中目标时返回 为 true 的截断原语。 + /// + public MotionPrimitive Generate( + Pose2D start, + double curvaturePerMeter, + TravelDirection direction, + PlanningGridMap map, + VehicleParameters vehicle, + HybridAStarConfiguration configuration, + Pose2D goal, + GoalDirectionConstraint goalDirection) + { + if (!IsValidInput(start, curvaturePerMeter, direction, map, vehicle, configuration, goal, goalDirection)) return null; + + if (GoalToleranceChecker.IsSatisfied(start, goal, configuration, direction, goalDirection)) + return new MotionPrimitive(start, direction, curvaturePerMeter, 0d, Array.Empty(), Array.Empty(), true); + + double pointStepMeters = Math.Min(configuration.IntegrationStepMeters, + Math.Min(configuration.MaximumCollisionCheckStepMeters, map.ResolutionMeters / 2d)); + if (!NumericGuard.IsPositiveFinite(pointStepMeters)) return null; + + var points = new List(); + var bodyClearancesMeters = new List(); + Pose2D previous = start; + double actualLengthMeters = 0d; + while (actualLengthMeters < configuration.PrimitiveLengthMeters) + { + double remainingLengthMeters = configuration.PrimitiveLengthMeters - actualLengthMeters; + double stepMeters = Math.Min(pointStepMeters, remainingLengthMeters); + if (!NumericGuard.IsPositiveFinite(stepMeters)) return null; + + Pose2D next = Integrate(previous, curvaturePerMeter, direction, stepMeters); + if (!IsFinitePose(next)) return null; + if (!_collisionChecker.IsSweptMotionCollisionFree(previous, next, map, vehicle, stepMeters, out double bodyClearanceMeters)) return null; + + actualLengthMeters += stepMeters; + points.Add(next); + bodyClearancesMeters.Add(bodyClearanceMeters); + if (GoalToleranceChecker.IsSatisfied(next, goal, configuration, direction, goalDirection)) + return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, true); + + previous = next; + } + + return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, false); + } + + /// + /// 获取从最大负曲率到最大正曲率均匀分布的候选曲率等级。 + /// 参数:vehicle 提供保守最大曲率;configuration 的曲率等级数必须为不小于 3 的奇数。 + /// 返回:输入无效时为空只读列表;有效时长度等于配置等级数且中间等级恒为零曲率。 + /// + public IReadOnlyList GetCurvatureLevels(VehicleParameters vehicle, HybridAStarConfiguration configuration) + { + if (configuration == null || configuration.CurvatureLevelCount < 3 || configuration.CurvatureLevelCount % 2 == 0 || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return Array.Empty(); + + var levels = new double[configuration.CurvatureLevelCount]; + double increment = 2d * maximumCurvaturePerMeter / (levels.Length - 1d); + for (int index = 0; index < levels.Length; index++) levels[index] = -maximumCurvaturePerMeter + increment * index; + levels[levels.Length / 2] = 0d; + return levels; + } + + /// + /// 判断两个曲率等级是否允许在相邻原语间直接切换。 + /// 参数:previousLevelIndex 与 nextLevelIndex 为从零开始的等级索引。 + /// 返回:两个索引均非负且最多相差一个等级时为 true。 + /// + public static bool AreCurvatureLevelsAdjacent(int previousLevelIndex, int nextLevelIndex) + { + return previousLevelIndex >= 0 && nextLevelIndex >= 0 && Math.Abs(previousLevelIndex - nextLevelIndex) <= 1; + } + + private static bool IsValidInput( + Pose2D start, + double curvaturePerMeter, + TravelDirection direction, + PlanningGridMap map, + VehicleParameters vehicle, + HybridAStarConfiguration configuration, + Pose2D goal, + GoalDirectionConstraint goalDirection) + { + if (!IsFinitePose(start) || !IsFinitePose(goal) || map == null || vehicle == null || configuration == null || + !NumericGuard.IsFinite(curvaturePerMeter) || !IsTravelDirection(direction) || !IsGoalDirection(goalDirection) || + !NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) || + !NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) || + !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || + !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return false; + + return Math.Abs(curvaturePerMeter) <= maximumCurvaturePerMeter; + } + + private static Pose2D Integrate(Pose2D previous, double curvaturePerMeter, TravelDirection direction, double stepMeters) + { + double signedDistanceMeters = direction == TravelDirection.Forward ? stepMeters : -stepMeters; + double nextHeadingRadians = AngleMath.NormalizeRadians(previous.Heading + curvaturePerMeter * signedDistanceMeters); + if (Math.Abs(curvaturePerMeter) < StraightCurvatureThreshold) + { + return new Pose2D( + previous.X + signedDistanceMeters * Math.Cos(previous.Heading), + previous.Y + signedDistanceMeters * Math.Sin(previous.Heading), + nextHeadingRadians); + } + + return new Pose2D( + previous.X + (Math.Sin(nextHeadingRadians) - Math.Sin(previous.Heading)) / curvaturePerMeter, + previous.Y - (Math.Cos(nextHeadingRadians) - Math.Cos(previous.Heading)) / curvaturePerMeter, + nextHeadingRadians); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs new file mode 100644 index 0000000..00babd0 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs @@ -0,0 +1,98 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 将运动原语的长度、方向、曲率和净空转换为统一的等效米搜索代价。 +/// 此类型不修改搜索状态;换向标记必须由调用方依据相邻原语的方向关系提供。 +/// +public sealed class SearchCostCalculator +{ + /// 创建使用调用时配置参数的等效米代价计算器。 + public SearchCostCalculator() + { + } + + /// + /// 计算一条运动原语的等效米增量代价。 + /// 参数:lengthMeters 为非负弧长 m;direction 为原语方向;isGearSwitch 表示该原语前是否换向; + /// curvaturePerMeter 与 maximumCurvaturePerMeter 的单位为 1/m;curvatureLevelDelta 为相邻曲率等级差; + /// bodyClearanceMeters 为非负车体保守净空 m,可为正无穷;configuration 提供非负权重和惩罚。 + /// 返回:有限且非负的等效米代价。 + /// 失败:任一数值、方向或权重无效时抛出 。 + /// + public double Calculate( + double lengthMeters, + TravelDirection direction, + bool isGearSwitch, + double curvaturePerMeter, + double maximumCurvaturePerMeter, + int curvatureLevelDelta, + double bodyClearanceMeters, + HybridAStarConfiguration configuration) + { + ValidateInput(lengthMeters, direction, curvaturePerMeter, maximumCurvaturePerMeter, bodyClearanceMeters, configuration); + + double directionMultiplier = direction == TravelDirection.Reverse ? configuration.ReverseCostMultiplier : 1d; + double normalizedCurvature = Math.Abs(curvaturePerMeter / maximumCurvaturePerMeter); + double clearanceDeficit = GetClearanceDeficit(bodyClearanceMeters, configuration.ClearanceCostDistanceMeters); + double levelDelta = Math.Abs((double)curvatureLevelDelta); + double motionCost = lengthMeters * directionMultiplier * + (1d + configuration.CurvatureMagnitudeWeight * normalizedCurvature + + configuration.ClearanceCostWeight * clearanceDeficit); + double gearSwitchCost = isGearSwitch ? configuration.GearSwitchPenaltyMeters : 0d; + double curvatureChangeCost = configuration.CurvatureChangePenaltyMetersPerLevel * levelDelta; + double totalCost = motionCost + gearSwitchCost + curvatureChangeCost; + if (!NumericGuard.IsFinite(totalCost) || totalCost < 0d) + throw new ArgumentOutOfRangeException(nameof(lengthMeters), "The calculated cost must remain finite and non-negative."); + + return totalCost; + } + + private static void ValidateInput( + double lengthMeters, + TravelDirection direction, + double curvaturePerMeter, + double maximumCurvaturePerMeter, + double bodyClearanceMeters, + HybridAStarConfiguration configuration) + { + if (!NumericGuard.IsFinite(lengthMeters) || lengthMeters < 0d) + throw new ArgumentOutOfRangeException(nameof(lengthMeters)); + if (direction != TravelDirection.Forward && direction != TravelDirection.Reverse) + throw new ArgumentOutOfRangeException(nameof(direction)); + if (!NumericGuard.IsFinite(curvaturePerMeter)) + throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); + if (!NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter)) + throw new ArgumentOutOfRangeException(nameof(maximumCurvaturePerMeter)); + if (Math.Abs(curvaturePerMeter) > maximumCurvaturePerMeter) + throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); + if (double.IsNaN(bodyClearanceMeters) || bodyClearanceMeters < 0d) + throw new ArgumentOutOfRangeException(nameof(bodyClearanceMeters)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + ValidateNonNegativeFinite(configuration.HeuristicWeight, nameof(configuration.HeuristicWeight)); + if (!NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier)) + throw new ArgumentOutOfRangeException(nameof(configuration.ReverseCostMultiplier)); + ValidateNonNegativeFinite(configuration.GearSwitchPenaltyMeters, nameof(configuration.GearSwitchPenaltyMeters)); + ValidateNonNegativeFinite(configuration.CurvatureMagnitudeWeight, nameof(configuration.CurvatureMagnitudeWeight)); + ValidateNonNegativeFinite(configuration.CurvatureChangePenaltyMetersPerLevel, nameof(configuration.CurvatureChangePenaltyMetersPerLevel)); + ValidateNonNegativeFinite(configuration.ClearanceCostWeight, nameof(configuration.ClearanceCostWeight)); + if (!NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters)) + throw new ArgumentOutOfRangeException(nameof(configuration.ClearanceCostDistanceMeters)); + } + + private static void ValidateNonNegativeFinite(double value, string parameterName) + { + if (!NumericGuard.IsFinite(value) || value < 0d) + throw new ArgumentOutOfRangeException(parameterName); + } + + private static double GetClearanceDeficit(double bodyClearanceMeters, double clearanceCostDistanceMeters) + { + if (double.IsPositiveInfinity(bodyClearanceMeters)) return 0d; + double deficit = 1d - bodyClearanceMeters / clearanceCostDistanceMeters; + return deficit > 0d ? deficit : 0d; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs new file mode 100644 index 0000000..63872e2 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; + +/// Clumsy 粗路径手动测试可选择的固定场景。 +public enum CoarsePathTestScenario +{ + /// 明确允许的空地图直达场景。 + ExplicitEmpty, + + /// 由中央矩形阻断直线的绕行场景。 + RectangleDetour, + + /// 同时包含手工圆形、矩形与 TwoLeg 快照的多来源场景。 + ManualAndTwoLeg, + + /// 与矩形绕行输入完全一致,用于在同一服务中验证输入缓存命中。 + CacheHit, + + /// 起步前进、终点倒车进入的换向场景。 + ReverseGearSwitch, + + /// 由贯穿边界的障碍带分隔起终点的无解场景。 + NoFeasiblePath, +} + +/// 手动障碍物输入支持的世界几何类型。 +public enum ManualCoarsePathObstacleKind +{ + /// 由世界中心和半径定义的圆形障碍物。 + Circle, + + /// 由世界中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。 + AxisAlignedRectangle, +} + +/// +/// 手动粗路径测试的不可变障碍物输入。 +/// 所有中心和尺寸均使用世界 mm;矩形始终与世界坐标轴平行,不包含旋转角。 +/// +public sealed class ManualCoarsePathObstacle +{ + private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters, + double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters) + { + Kind = kind; + CenterXMillimeters = centerXMillimeters; + CenterYMillimeters = centerYMillimeters; + SizeXMillimeters = sizeXMillimeters; + SizeYMillimeters = sizeYMillimeters; + } + + /// 障碍物的支持几何类型。 + public ManualCoarsePathObstacleKind Kind { get; } + + /// 几何中心世界 X 坐标,单位 mm。 + public double CenterXMillimeters { get; } + + /// 几何中心世界 Y 坐标,单位 mm。 + public double CenterYMillimeters { get; } + + /// 圆形时为半径,矩形时为 X 方向长度;单位 mm。 + public double SizeXMillimeters { get; } + + /// 圆形时为半径,矩形时为 Y 方向宽度;单位 mm。 + public double SizeYMillimeters { get; } + + /// 创建圆形障碍物。参数:圆心和半径均使用世界 mm,半径必须为有限正数。 + public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters, + double radiusMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters, + centerYMillimeters, radiusMillimeters, radiusMillimeters); + } + + /// + /// 创建轴对齐矩形障碍物。 + /// 参数:中心、X 方向长度和 Y 方向宽度均使用世界 mm;两个尺寸必须为有限正数。 + /// + public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters, + double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters)); + EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle, + centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters); + } + + private static void EnsureFinite(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); + } + + private static void EnsurePositiveFinite(double value, string parameterName) + { + EnsureFinite(value, parameterName); + if (value <= 0d) + throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); + } +} + +/// +/// Clumsy 粗路径测试的纯输入工厂。 +/// 固定场景不读取 UI、传感器、定位或时钟;传入 AMR 位姿的手动入口仅在此处完成世界 mm/deg 到核心 m/rad 的转换。 +/// +public static class CoarsePathScenarioFactory +{ + private const float MapXMinMillimeters = 0f; + private const float MapXMaxMillimeters = 6000f; + private const float MapYMinMillimeters = 0f; + private const float MapYMaxMillimeters = 4000f; + private const float ResolutionMillimeters = 50f; + private const double MillimetersPerMeter = 1000d; + private const double DegreesToRadians = Math.PI / 180d; + private const double ManualMapPaddingMillimeters = 8000d; + private const int MaximumManualObstacleCount = 20; + + /// + /// 创建一个新的固定测试业务请求。 + /// 返回:每次调用都返回独立的可变请求对象,供调用方安全地传入同一个长期存活的规划服务。 + /// + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) + { + return CreateCore(scenario, null); + } + + /// + /// 创建以当前 AMR 世界位姿为起点的固定测试业务请求。 + /// 参数:X/Y 使用世界 mm,航向使用 deg;地图、目标和障碍物仅随 AMR 坐标平移,TwoLeg 朝向保持不变。 + /// + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, + double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees) + { + return CreateCore(scenario, + new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees)); + } + + private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, FixedScenarioAnchor anchor) + { + switch (scenario) + { + case CoarsePathTestScenario.ExplicitEmpty: + return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.RectangleDetour: + case CoarsePathTestScenario.CacheHit: + return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.ManualAndTwoLeg: + return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor)); + case CoarsePathTestScenario.ReverseGearSwitch: + return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.NoFeasiblePath: + return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + default: + throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + + /// + /// 创建传入 AMR 世界位姿和手动世界终点的空图演示请求。 + /// 参数:X/Y 使用世界 mm,航向使用 deg;返回请求中的 使用世界 m/rad。 + /// 注意:这是坐标、路径和取消流程的演示空图,不能表示现场不存在障碍物。 + /// + public static CoarsePathPlanningJob CreateManualGoalDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) + { + return CreateManualObstacleDemo(startXMillimeters, startYMillimeters, startHeadingDegrees, + goalXMillimeters, goalYMillimeters, goalHeadingDegrees, + Array.Empty(), 0L); + } + + /// + /// 创建传入 AMR 世界位姿、手动世界终点和手动障碍物快照的测试请求。 + /// 参数:位姿 X/Y、障碍物中心和尺寸使用世界 mm,航向使用 deg;返回的 使用 m/rad。 + /// 障碍物非空时 obstacleSnapshotVersion 必须为正数,以避免长期服务错误复用旧地图;零障碍物才创建显式空图演示。 + /// + public static CoarsePathPlanningJob CreateManualObstacleDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees, + IReadOnlyList obstacles, long obstacleSnapshotVersion) + { + EnsureFinite(startXMillimeters, nameof(startXMillimeters)); + EnsureFinite(startYMillimeters, nameof(startYMillimeters)); + EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); + EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); + EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); + EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); + + if (obstacles == null) throw new ArgumentNullException(nameof(obstacles)); + if (obstacles.Count > MaximumManualObstacleCount) + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle count exceeds the supported limit."); + if (obstacles.Count != 0 && obstacleSnapshotVersion <= 0L) + throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version."); + + return CreateJob( + CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters, + obstacles, obstacleSnapshotVersion), + ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), + ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), + null, + GoalDirectionConstraint.Any); + } + + private static CoarsePathPlanningJob CreateExplicitEmpty(FixedScenarioTransform transform) + { + return CreateJob( + CreateMap(true, Array.Empty(), transform), + transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), + null, + GoalDirectionConstraint.Forward); + } + + private static CoarsePathPlanningJob CreateRectangleDetour(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] + { + new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f), + transform.Y(1200f), transform.Y(2800f)), + }), + }; + CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); + // 固定绕行场景保留最优启发式;30 秒覆盖较慢测试环境,UI 仍可随时取消。 + job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d); + return job; + } + + private static CoarsePathPlanningJob CreateManualAndTwoLeg(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 2L, true, new IMapObstacle[] + { + new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f), + new AxisAlignedRectangleObstacle(transform.X(3000f), transform.X(3600f), + transform.Y(2000f), transform.Y(2600f)), + }), + new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true, + transform.X(3900f), transform.Y(2500f), 0d, + -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), + }; + CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 1d, 0d), + transform.Pose(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); + // 多来源场景的最优绕行会受机器负载影响;放宽演示总预算但保留全部碰撞与目标判定。 + job.Configuration.SearchTimeout = TimeSpan.FromSeconds(15d); + return job; + } + + private static CoarsePathPlanningJob CreateReverseGearSwitch(FixedScenarioTransform transform) + { + return CreateJob( + CreateMap(true, Array.Empty(), transform), + transform.Pose(1d, 2d, 0d), + transform.Pose(4d, 2d, 0d), + TravelDirection.Forward, + GoalDirectionConstraint.Reverse); + } + + private static CoarsePathPlanningJob CreateNoFeasiblePath(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 3L, true, new IMapObstacle[] + { + new AxisAlignedRectangleObstacle(transform.X(2900f), transform.X(3100f), + transform.Y(0f), transform.Y(4000f)), + }), + }; + return CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), + null, GoalDirectionConstraint.Forward); + } + + private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, + TravelDirection? startDirection, GoalDirectionConstraint goalDirection) + { + return new CoarsePathPlanningJob + { + MapRequest = mapRequest, + Start = start, + Goal = goal, + Vehicle = new VehicleParameters + { + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, + }, + Configuration = new HybridAStarConfiguration(), + StartDirection = startDirection, + GoalDirection = goalDirection, + }; + } + + private static PlanningMapRequest CreateMap(bool allowExplicitEmptyMap, IReadOnlyList sources, + FixedScenarioTransform transform) + { + return new PlanningMapRequest + { + Bounds = new MapBoundsMm(transform.X(MapXMinMillimeters), transform.X(MapXMaxMillimeters), + transform.Y(MapYMinMillimeters), transform.Y(MapYMaxMillimeters)), + ResolutionMm = ResolutionMillimeters, + ObstacleSources = sources, + AllowExplicitEmptyMap = allowExplicitEmptyMap, + }; + } + + private sealed class FixedScenarioAnchor + { + public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees) + { + EnsureFinite(xMillimeters, nameof(xMillimeters)); + EnsureFinite(yMillimeters, nameof(yMillimeters)); + EnsureFinite(headingDegrees, nameof(headingDegrees)); + XMillimeters = xMillimeters; + YMillimeters = yMillimeters; + HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians); + } + + public double XMillimeters { get; } + public double YMillimeters { get; } + public double HeadingRadians { get; } + } + + private sealed class FixedScenarioTransform + { + private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians) + { + DeltaXMillimeters = deltaXMillimeters; + DeltaYMillimeters = deltaYMillimeters; + HeadingDeltaRadians = headingDeltaRadians; + } + + public double DeltaXMillimeters { get; } + public double DeltaYMillimeters { get; } + public double HeadingDeltaRadians { get; } + + public static FixedScenarioTransform From(double baselineStartXMillimeters, + double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor) + { + if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d); + return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters, + anchor.YMillimeters - baselineStartYMillimeters, + NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians)); + } + + public float X(float value) + { + return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); + } + + public float Y(float value) + { + return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); + } + + public Pose2D Pose(double xMeters, double yMeters, double headingRadians) + { + return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter, + (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter, + NormalizeRadians(headingRadians + HeadingDeltaRadians)); + } + } + + private static PlanningMapRequest CreateManualDemoMap(double startXMillimeters, double startYMillimeters, + double goalXMillimeters, double goalYMillimeters, IReadOnlyList obstacles, + long obstacleSnapshotVersion) + { + double minimumX = Math.Min(startXMillimeters, goalXMillimeters); + double maximumX = Math.Max(startXMillimeters, goalXMillimeters); + double minimumY = Math.Min(startYMillimeters, goalYMillimeters); + double maximumY = Math.Max(startYMillimeters, goalYMillimeters); + for (int index = 0; index < obstacles.Count; index++) + { + ManualCoarsePathObstacle obstacle = obstacles[index] ?? + throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); + double halfX; + double halfY; + switch (obstacle.Kind) + { + case ManualCoarsePathObstacleKind.Circle: + EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); + halfX = obstacle.SizeXMillimeters; + halfY = obstacle.SizeYMillimeters; + break; + case ManualCoarsePathObstacleKind.AxisAlignedRectangle: + EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); + EnsurePositiveFinite(obstacle.SizeYMillimeters, nameof(obstacles)); + halfX = obstacle.SizeXMillimeters / 2d; + halfY = obstacle.SizeYMillimeters / 2d; + break; + default: + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); + } + + minimumX = Math.Min(minimumX, obstacle.CenterXMillimeters - halfX); + maximumX = Math.Max(maximumX, obstacle.CenterXMillimeters + halfX); + minimumY = Math.Min(minimumY, obstacle.CenterYMillimeters - halfY); + maximumY = Math.Max(maximumY, obstacle.CenterYMillimeters + halfY); + } + + float xMin = ToGridLowerBound(minimumX - ManualMapPaddingMillimeters); + float xMax = ToGridUpperBound(maximumX + ManualMapPaddingMillimeters); + float yMin = ToGridLowerBound(minimumY - ManualMapPaddingMillimeters); + float yMax = ToGridUpperBound(maximumY + ManualMapPaddingMillimeters); + bool isExplicitEmptyMap = obstacles.Count == 0; + return new PlanningMapRequest + { + Bounds = new MapBoundsMm(xMin, xMax, yMin, yMax), + ResolutionMm = ResolutionMillimeters, + ObstacleSources = isExplicitEmptyMap ? Array.Empty() : + new IMapObstacleSource[] + { + new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, + ConvertManualObstacles(obstacles)), + }, + AllowExplicitEmptyMap = isExplicitEmptyMap, + }; + } + + private static IMapObstacle[] ConvertManualObstacles(IReadOnlyList obstacles) + { + var result = new IMapObstacle[obstacles.Count]; + for (int index = 0; index < obstacles.Count; index++) + { + ManualCoarsePathObstacle obstacle = obstacles[index] ?? + throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); + switch (obstacle.Kind) + { + case ManualCoarsePathObstacleKind.Circle: + result[index] = new CircleObstacle(ToFiniteFloat(obstacle.CenterXMillimeters, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters, nameof(obstacles)), + ToFiniteFloat(obstacle.SizeXMillimeters, nameof(obstacles))); + break; + case ManualCoarsePathObstacleKind.AxisAlignedRectangle: + double halfX = obstacle.SizeXMillimeters / 2d; + double halfY = obstacle.SizeYMillimeters / 2d; + result[index] = new AxisAlignedRectangleObstacle( + ToFiniteFloat(obstacle.CenterXMillimeters - halfX, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterXMillimeters + halfX, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters - halfY, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters + halfY, nameof(obstacles))); + break; + default: + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); + } + } + return result; + } + + private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) + { + return new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, + headingDegrees * DegreesToRadians); + } + + private static double NormalizeRadians(double angle) + { + double normalized = angle % (2d * Math.PI); + if (normalized <= -Math.PI) return normalized + 2d * Math.PI; + return normalized > Math.PI ? normalized - 2d * Math.PI : normalized; + } + + private static float ToGridLowerBound(double millimeters) + { + double rounded = Math.Floor(millimeters / ResolutionMillimeters) * ResolutionMillimeters; + return ToFiniteFloat(rounded, nameof(millimeters)); + } + + private static float ToGridUpperBound(double millimeters) + { + double rounded = Math.Ceiling(millimeters / ResolutionMillimeters) * ResolutionMillimeters; + return ToFiniteFloat(rounded, nameof(millimeters)); + } + + private static float ToFiniteFloat(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue) + throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate."); + return (float)value; + } + + private static void EnsureFinite(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); + } + + private static void EnsurePositiveFinite(double value, string parameterName) + { + EnsureFinite(value, parameterName); + if (value <= 0d) + throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs new file mode 100644 index 0000000..2fa47d5 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs @@ -0,0 +1,628 @@ +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 + " 必须是有限数。"); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs new file mode 100644 index 0000000..c403d4a --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs @@ -0,0 +1,160 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// +/// 对以车辆几何中心表示的扩大矩形执行连续碰撞检查。 +/// 地图查询使用 m;任何地图外车辆部分、占据格相交或擦边均按碰撞处理。 +/// +public sealed class FootprintCollisionChecker +{ + /// 创建连续车辆碰撞检查器。 + public FootprintCollisionChecker() + { + } + + /// + /// 判断单个车辆位姿是否无碰撞。 + /// 参数:pose 为车辆几何中心的世界 m/rad 位姿;map 为不可变规划地图;vehicle 为车辆尺寸; + /// additionalMarginMeters 为临时额外安全余量,单位 m;bodyClearanceMeters 输出不含该临时余量的保守车体净空下界,单位 m。 + /// 返回:扩大车辆矩形完整位于地图内且不与任何占据格相交或擦边时为 true;无效输入保守地返回 false。 + /// + public bool IsPoseCollisionFree(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, + double additionalMarginMeters, out double bodyClearanceMeters) + { + bodyClearanceMeters = 0d; + if (map == null || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d || + !VehicleFootprint.TryCreate(pose, vehicle, 0d, out VehicleFootprint bodyFootprint) || + !VehicleFootprint.TryCreate(pose, vehicle, additionalMarginMeters, out VehicleFootprint checkedFootprint)) + return false; + + if (!AreCornersInsideMap(checkedFootprint, map)) return false; + + double centerDistanceMeters = map.GetConservativeObstacleDistanceMeters(pose.X, pose.Y); + bodyClearanceMeters = GetBodyClearance(centerDistanceMeters, bodyFootprint.CircumscribedRadiusMeters); + if (centerDistanceMeters > checkedFootprint.CircumscribedRadiusMeters) return true; + + return !IntersectsOccupiedCell(checkedFootprint, map); + } + + /// + /// 判断两个位姿之间的平移和转向扫掠是否无碰撞。 + /// 参数:from、to 为世界 m/rad 位姿;maximumCenterStepMeters 为允许的最大中心采样间距,单位 m; + /// minimumBodyClearanceMeters 输出沿途不含临时扫掠余量的保守车体净空下界,单位 m。 + /// 返回:端点和每个分段扫掠均无碰撞时为 true;无效输入、地图外或任一中间碰撞时返回 false。 + /// + public bool IsSweptMotionCollisionFree(Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, + double maximumCenterStepMeters, out double minimumBodyClearanceMeters) + { + minimumBodyClearanceMeters = 0d; + if (map == null || from == null || to == null || !NumericGuard.IsFinite(maximumCenterStepMeters) || maximumCenterStepMeters <= 0d || + !NumericGuard.IsFinite(from.X) || !NumericGuard.IsFinite(from.Y) || !NumericGuard.IsFinite(from.Heading) || + !NumericGuard.IsFinite(to.X) || !NumericGuard.IsFinite(to.Y) || !NumericGuard.IsFinite(to.Heading) || + !VehicleFootprint.TryCreate(from, vehicle, 0d, out VehicleFootprint bodyFootprint)) + return false; + + double allowedStepMeters = Math.Min(maximumCenterStepMeters, map.ResolutionMeters / 2d); + if (!NumericGuard.IsPositiveFinite(allowedStepMeters)) return false; + + if (!IsPoseCollisionFree(from, map, vehicle, 0d, out double fromClearanceMeters)) return false; + minimumBodyClearanceMeters = fromClearanceMeters; + + double deltaX = to.X - from.X; + double deltaY = to.Y - from.Y; + double centerDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + if (!NumericGuard.IsFinite(centerDistanceMeters)) return false; + double headingDeltaRadians = AngleMath.ShortestSignedDifference(from.Heading, to.Heading); + if (!NumericGuard.IsFinite(headingDeltaRadians)) return false; + double rawSegmentCount = Math.Ceiling(centerDistanceMeters / allowedStepMeters); + if (!NumericGuard.IsFinite(rawSegmentCount) || rawSegmentCount > int.MaxValue) return false; + int segmentCount = Math.Max(1, (int)rawSegmentCount); + + Pose2D previousPose = from; + for (int segment = 1; segment <= segmentCount; segment++) + { + double endFraction = (double)segment / segmentCount; + double middleFraction = ((double)segment - 0.5d) / segmentCount; + var currentPose = new Pose2D( + from.X + deltaX * endFraction, + from.Y + deltaY * endFraction, + from.Heading + headingDeltaRadians * endFraction); + var middlePose = new Pose2D( + from.X + deltaX * middleFraction, + from.Y + deltaY * middleFraction, + from.Heading + headingDeltaRadians * middleFraction); + double segmentDeltaX = currentPose.X - previousPose.X; + double segmentDeltaY = currentPose.Y - previousPose.Y; + double segmentCenterDisplacementMeters = Math.Sqrt(segmentDeltaX * segmentDeltaX + segmentDeltaY * segmentDeltaY); + double segmentHeadingDeltaRadians = currentPose.Heading - previousPose.Heading; + double temporaryMarginMeters = 0.5d * (segmentCenterDisplacementMeters + + bodyFootprint.CircumscribedRadiusMeters * Math.Abs(segmentHeadingDeltaRadians)); + if (!NumericGuard.IsFinite(temporaryMarginMeters) || + !IsPoseCollisionFree(middlePose, map, vehicle, temporaryMarginMeters, out double middleClearanceMeters)) + return false; + minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, middleClearanceMeters); + previousPose = currentPose; + } + + if (!IsPoseCollisionFree(to, map, vehicle, 0d, out double toClearanceMeters)) return false; + minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, toClearanceMeters); + return true; + } + + private static bool AreCornersInsideMap(VehicleFootprint footprint, PlanningGridMap map) + { + for (int index = 0; index < 4; index++) + { + footprint.GetCorner(index, out double cornerX, out double cornerY); + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + return true; + } + + private static double GetBodyClearance(double centerDistanceMeters, double bodyRadiusMeters) + { + if (double.IsPositiveInfinity(centerDistanceMeters)) return double.PositiveInfinity; + if (!NumericGuard.IsFinite(centerDistanceMeters) || !NumericGuard.IsFinite(bodyRadiusMeters)) return 0d; + return Math.Max(0d, centerDistanceMeters - bodyRadiusMeters); + } + + private static bool IntersectsOccupiedCell(VehicleFootprint footprint, PlanningGridMap map) + { + GetCellRange(map, footprint.MinX, footprint.MaxX, footprint.MinY, footprint.MaxY, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + if (!map.IsOccupied(row, col)) continue; + GetCellBoundsMeters(map, row, col, out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY); + if (OrientedRectangleCellIntersection.Intersects(footprint, cellMinX, cellMaxX, cellMinY, cellMaxY)) return true; + } + return false; + } + + private static void GetCellRange(PlanningGridMap map, double minX, double maxX, double minY, double maxY, + out int firstRow, out int lastRow, out int firstCol, out int lastCol) + { + double minimumMapX = map.Bounds.XMin / 1000d; + double minimumMapY = map.Bounds.YMin / 1000d; + firstCol = Clamp((int)Math.Floor((minX - minimumMapX) / map.ResolutionMeters) - 1, 0, map.Cols - 1); + lastCol = Clamp((int)Math.Floor((maxX - minimumMapX) / map.ResolutionMeters), 0, map.Cols - 1); + firstRow = Clamp((int)Math.Floor((minY - minimumMapY) / map.ResolutionMeters) - 1, 0, map.Rows - 1); + lastRow = Clamp((int)Math.Floor((maxY - minimumMapY) / map.ResolutionMeters), 0, map.Rows - 1); + } + + private static void GetCellBoundsMeters(PlanningGridMap map, int row, int col, + out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY) + { + cellMinX = map.Bounds.XMin / 1000d + col * map.ResolutionMeters; + cellMinY = map.Bounds.YMin / 1000d + row * map.ResolutionMeters; + cellMaxX = Math.Min(map.Bounds.XMax / 1000d, cellMinX + map.ResolutionMeters); + cellMaxY = Math.Min(map.Bounds.YMax / 1000d, cellMinY + map.ResolutionMeters); + } + + private static int Clamp(int value, int minimum, int maximum) + { + return value < minimum ? minimum : value > maximum ? maximum : value; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs new file mode 100644 index 0000000..53234be --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs @@ -0,0 +1,33 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// 旋转矩形与轴对齐栅格的分离轴相交判定。 +internal static class OrientedRectangleCellIntersection +{ + /// 任一投影轴没有严格分离时返回 true;擦边按相交处理。 + public static bool Intersects(VehicleFootprint rectangle, double cellMinX, double cellMaxX, double cellMinY, double cellMaxY) + { + if (rectangle == null || cellMaxX < cellMinX || cellMaxY < cellMinY) return false; + double cellCenterX = (cellMinX + cellMaxX) / 2d; + double cellCenterY = (cellMinY + cellMaxY) / 2d; + double cellHalfX = (cellMaxX - cellMinX) / 2d; + double cellHalfY = (cellMaxY - cellMinY) / 2d; + + return !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLongitudinalX, rectangle.AxisLongitudinalY) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLateralX, rectangle.AxisLateralY) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 1d, 0d) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 0d, 1d); + } + + private static bool HasStrictSeparation(VehicleFootprint rectangle, double cellCenterX, double cellCenterY, double cellHalfX, double cellHalfY, + double axisX, double axisY) + { + double rectangleCenter = rectangle.CenterX * axisX + rectangle.CenterY * axisY; + double cellCenter = cellCenterX * axisX + cellCenterY * axisY; + double rectangleRadius = rectangle.HalfLengthMeters * Math.Abs(rectangle.AxisLongitudinalX * axisX + rectangle.AxisLongitudinalY * axisY) + + rectangle.HalfWidthMeters * Math.Abs(rectangle.AxisLateralX * axisX + rectangle.AxisLateralY * axisY); + double cellRadius = cellHalfX * Math.Abs(axisX) + cellHalfY * Math.Abs(axisY); + return Math.Abs(rectangleCenter - cellCenter) > rectangleRadius + cellRadius; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs new file mode 100644 index 0000000..aa88e55 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs @@ -0,0 +1,90 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// 以车辆几何中心为原点的扩大旋转矩形。 +internal sealed class VehicleFootprint +{ + private VehicleFootprint(Pose2D pose, double halfLengthMeters, double halfWidthMeters) + { + CenterX = pose.X; + CenterY = pose.Y; + HalfLengthMeters = halfLengthMeters; + HalfWidthMeters = halfWidthMeters; + AxisLongitudinalX = Math.Cos(pose.Heading); + AxisLongitudinalY = Math.Sin(pose.Heading); + AxisLateralX = -AxisLongitudinalY; + AxisLateralY = AxisLongitudinalX; + CircumscribedRadiusMeters = Math.Sqrt(halfLengthMeters * halfLengthMeters + halfWidthMeters * halfWidthMeters); + + double minX = double.PositiveInfinity; + double maxX = double.NegativeInfinity; + double minY = double.PositiveInfinity; + double maxY = double.NegativeInfinity; + for (int index = 0; index < 4; index++) + { + GetCorner(index, out double x, out double y); + minX = Math.Min(minX, x); + maxX = Math.Max(maxX, x); + minY = Math.Min(minY, y); + maxY = Math.Max(maxY, y); + } + MinX = minX; + MaxX = maxX; + MinY = minY; + MaxY = maxY; + } + + public double CenterX { get; } + public double CenterY { get; } + public double HalfLengthMeters { get; } + public double HalfWidthMeters { get; } + public double AxisLongitudinalX { get; } + public double AxisLongitudinalY { get; } + public double AxisLateralX { get; } + public double AxisLateralY { get; } + public double CircumscribedRadiusMeters { get; } + public double MinX { get; } + public double MaxX { get; } + public double MinY { get; } + public double MaxY { get; } + + /// 创建包含车辆安全余量和临时扫掠余量的矩形。 + public static bool TryCreate(Pose2D pose, VehicleParameters vehicle, double additionalMarginMeters, out VehicleFootprint footprint) + { + footprint = null; + if (pose == null || vehicle == null || !NumericGuard.IsFinite(pose.X) || !NumericGuard.IsFinite(pose.Y) || + !NumericGuard.IsFinite(pose.Heading) || !NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || + !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || + vehicle.SafetyMarginMeters < 0d || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d) + return false; + + double totalMarginMeters = vehicle.SafetyMarginMeters + additionalMarginMeters; + if (!NumericGuard.IsFinite(totalMarginMeters)) return false; + double halfLengthMeters = vehicle.LengthMeters / 2d + totalMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + totalMarginMeters; + if (!NumericGuard.IsPositiveFinite(halfLengthMeters) || !NumericGuard.IsPositiveFinite(halfWidthMeters)) return false; + + footprint = new VehicleFootprint(pose, halfLengthMeters, halfWidthMeters); + return NumericGuard.IsFinite(footprint.CircumscribedRadiusMeters) && NumericGuard.IsFinite(footprint.MinX) && + NumericGuard.IsFinite(footprint.MaxX) && NumericGuard.IsFinite(footprint.MinY) && NumericGuard.IsFinite(footprint.MaxY); + } + + /// 获取指定角点。索引按逆时针顺序为 0 到 3。 + public void GetCorner(int index, out double x, out double y) + { + double longitudinalSign; + double lateralSign; + switch (index) + { + case 0: longitudinalSign = 1d; lateralSign = 1d; break; + case 1: longitudinalSign = -1d; lateralSign = 1d; break; + case 2: longitudinalSign = -1d; lateralSign = -1d; break; + case 3: longitudinalSign = 1d; lateralSign = -1d; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + x = CenterX + longitudinalSign * HalfLengthMeters * AxisLongitudinalX + lateralSign * HalfWidthMeters * AxisLateralX; + y = CenterY + longitudinalSign * HalfLengthMeters * AxisLongitudinalY + lateralSign * HalfWidthMeters * AxisLateralY; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs new file mode 100644 index 0000000..88bed55 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs @@ -0,0 +1,40 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// +/// 从车辆参数提取运动学限制的辅助方法。 +/// 曲率单位为 1/m;当最大曲率与最小转弯半径同时给出时,始终选择更保守的较小曲率。 +/// +public static class VehicleKinematics +{ + /// + /// 尝试获取车辆允许的最大绝对曲率。 + /// 参数:vehicle 为车辆参数;maximumCurvaturePerMeter 为输出的正有限曲率,单位 1/m。 + /// 返回:至少提供一种正有限曲率限制时为 true;任一已提供限制无效或两种限制均未提供时为 false。 + /// + public static bool TryGetMaximumCurvaturePerMeter(VehicleParameters vehicle, out double maximumCurvaturePerMeter) + { + maximumCurvaturePerMeter = 0d; + if (vehicle == null) return false; + + bool hasMaximumCurvature = vehicle.MaximumCurvaturePerMeter.HasValue; + bool hasMinimumRadius = vehicle.MinimumTurningRadiusMeters.HasValue; + if (hasMaximumCurvature && !NumericGuard.IsPositiveFinite(vehicle.MaximumCurvaturePerMeter.Value)) return false; + if (hasMinimumRadius && !NumericGuard.IsPositiveFinite(vehicle.MinimumTurningRadiusMeters.Value)) return false; + if (!hasMaximumCurvature && !hasMinimumRadius) return false; + + if (hasMaximumCurvature && hasMinimumRadius) + { + maximumCurvaturePerMeter = System.Math.Min( + vehicle.MaximumCurvaturePerMeter.Value, + 1d / vehicle.MinimumTurningRadiusMeters.Value); + return true; + } + + maximumCurvaturePerMeter = hasMaximumCurvature + ? vehicle.MaximumCurvaturePerMeter.Value + : 1d / vehicle.MinimumTurningRadiusMeters.Value; + return NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html b/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html new file mode 100644 index 0000000..6269f51 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html @@ -0,0 +1,812 @@ + + + + + +AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案 + + + +
+

AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案

+

+ 面向四舵轮AMR的第一阶段工程实现:暂不考虑蟹行与纯横移,仅采用车式运动模型, + 支持前进、倒车及换向,输出供后续SQP使用的无时间空间粗路径。 + 本文统一整理已经确认的第1至第14步,作为软件设计、编码、调试与验收依据。 +

+
+ 步骤1–12:已确认 + 步骤13:性能优化TODO + 步骤14:接口与验收已确认 + 地图分辨率初值:0.05 m + 原语长度:0.50 m + 积分步长:0.05 m +
+
+ +
+ + +
+
+

0. 总体概览

+
+
车式运动
车头/车尾方向运动,不考虑蟹行
+
前进 + 倒车
允许在原语边界切换方向
+
空间粗路径
不包含速度、加速度、时间戳
+
+ +

0.1 总体职责边界

+ + + + + + +
模块主要职责不负责
Hybrid A*拓扑可达、无碰撞、基本运动学可行、前进/倒车结构、目标位置与航向接近最终速度、加速度、时间、四轮舵角与轮速
路径后处理回溯、稠密点恢复、关键点保留、弧长重采样、快速曲线平滑、完整复核动态约束和最终轨迹时间参数化
后续SQP终点精确收敛、速度/加速度/角速度/时间戳、换向点停车、动态可跟踪性重新决定绕障侧或凭空增加倒车拓扑
底盘逆解与控制四轮舵角、轮速分配和轨迹跟踪全局绕障搜索
+ +

0.2 总体数据流

+
+
感知障碍物 / 点云 → 过滤、投影到二维
+
二维占据栅格 + 障碍物距离场
+
起点、终点、车辆参数、规划配置
+
二维Dijkstra启发图 + Hybrid A*搜索
+
机会式/强制 Reeds–Shepp 终点连接
+
父节点回溯 + 0.05 m内部积分点恢复
+
关键点保留 + 分方向弧长重采样
+
三次B样条优先,局部Bézier/五次多项式,局部QP兜底
+
最终碰撞、净空、曲率、换向结构校验
+
输出空间粗路径 → 后续SQP
+
+ +
+第一版核心原则:先保证算法完整跑通、路径正确、可解释、可复现;性能优化整体放入第十三步TODO,待前12步稳定后再开展。 +
+
+ +
+

1任务边界与输出定义 已确认

+

1.1 车辆运动模式

+

目标平台为四舵轮AMR,但第一阶段只使用车式运动能力:

+
    +
  • 车辆沿车头或车尾方向运动;
  • +
  • 允许前进、倒车以及换向;
  • +
  • 暂不考虑蟹行、纯横移和任意方向全向运动;
  • +
  • 车体航向角始终表示车头朝向,即使车辆正在倒车。
  • +
+ +

1.2 Hybrid A*最小输出

+

输出空间路径点序列,至少包含:

+
位置:x, y
+车体航向:heading
+运动方向:Forward / Reverse
+累计弧长:s
+参考/几何曲率:kappa
+换向标记:isGearSwitchPoint
+安全净空:clearance
+

不输出速度、加速度、角速度、时间戳、四个舵轮角度与轮速。

+ +

1.3 成功判据

+
    +
  • 路径全程无碰撞且不进入未知区域;
  • +
  • 粗路径基本满足最大曲率约束;
  • +
  • 包含正确绕障侧、前进/后退和换向结构;
  • +
  • 终点位置与航向满足第九步终止规则;
  • +
  • 可作为后续SQP的有效初值。
  • +
+
+ +
+

2地图表达与环境输入 已确认

+

2.1 地图生成流程

+
原始点云 / 障碍信息 + ↓ 过滤噪声、地面、离群点 +投影到二维平面 + ↓ +二维占据栅格 Occupancy Grid + ↓ +障碍物距离场 Distance Field / EDT
+ +

2.2 栅格定义

+
    +
  • 地图分辨率初值:0.05 m
  • +
  • 状态:Free、Occupied、Unknown;
  • +
  • 第一版中Unknown按障碍物处理;
  • +
  • 地图包含Origin、Resolution、Width、Height及Version;
  • +
  • 算法内部不得写死分辨率,统一读取地图参数。
  • +
+ +

2.3 车体碰撞模型

+

AMR使用旋转矩形车体碰撞模型,不能只用中心点或单圆近似。安全余量主要加到车体矩形上:

+
CheckLength = VehicleLength + 2 × SafetyMargin
+CheckWidth  = VehicleWidth  + 2 × SafetyMargin
+

初始安全余量为0.03 m。避免地图膨胀与车体扩大同时重复计算同一余量。

+
+ +
+

3车辆运动模型与最大曲率 已确认

+

3.1 参考点

+

车辆状态参考点采用AMR几何中心。

+ +

3.2 基于路径弧长的运动学

+
dx/ds = cos(theta)
+dy/ds = sin(theta)
+dtheta/ds = kappa
+

其中theta为车头航向角,kappa为车体中心路径曲率。

+ +

3.3 最大曲率来源

+

必须同时支持两种来源:

+
    +
  1. 外部直接输入MaxCurvature,或输入MinTurningRadius并换算kappa_max = 1 / R_min
  2. +
  3. 由四轮转向几何调用ComputeMaxCurvature(VehicleKinematicParameters)计算。
  4. +
+

推荐三种模式:

+
ExternalOnly
+GeometryOnly
+ConservativeMinimum
+

当两种来源同时存在时,建议取更保守的较小值。若均无有效值,则返回配置错误。

+ +
+曲率变化率不在本步骤强制建模。第一版先保证曲率不超过上限;渐变曲率原语和实车舵轮速率约束作为后续升级。 +
+
+ +
+

4Hybrid A*搜索节点状态 已确认

+

4.1 连续状态

+
(x, y, theta, direction, curvatureIndex)
+
    +
  • x, y, theta保持连续值用于运动积分;
  • +
  • direction为Forward或Reverse;
  • +
  • curvatureIndex表示当前离散曲率等级。
  • +
+ +

4.2 Closed Set键

+
(ix, iy, iHeading, direction, curvatureIndex)
+

推荐初始离散:

+
    +
  • 位置Closed Set分辨率:0.05–0.10 m
  • +
  • 航向离散:
  • +
  • 方向和曲率等级均进入键,避免把运动状态不同的节点错误合并。
  • +
+ +

4.3 搜索元数据

+

节点还应保存G/H/F代价、父节点索引、生成当前节点的原语积分点、最小净空、终止原因等。这些属于搜索管理信息,不属于车辆物理状态。

+
+ +
+

5运动原语与扩展规则 已确认

+

5.1 第一版原语集合

+
{ -kappa_max, -0.5 kappa_max, 0, 0.5 kappa_max, kappa_max }
+

每种曲率均支持Forward与Reverse。

+ +

5.2 曲率切换限制

+

相邻原语曲率等级最多变化1级。例如:

+
允许:0 → 0.5κmax
+允许:0.5κmax → κmax
+不允许:κmax → -κmax
+

该限制可减少“左打死后立即右打死”的不合理跳变。

+ +

5.3 换向规则

+
    +
  • 前进/倒车只能在运动原语边界切换;
  • +
  • 起始曲率若上层可提供则使用实际值,否则默认0;
  • +
  • 第一版采用恒定曲率原语。
  • +
+ +
+TODO:当恒定曲率原语造成曲率跳变过大、平滑后仍不可跟踪或实车舵轮变化速度不足时,增加线性渐变曲率原语。 +
+
+ +
+

6原语长度、积分与终止 已确认

+

6.1 固定参数

+
PrimitiveLength = 0.50 m
+IntegrationStep = 0.05 m
+

完整原语最多包含10个内部积分点。只有原语终点进入Open List,内部积分点用于碰撞、净空和最终路径恢复。

+ +

6.2 终止条件

+ + + + + + + +
条件处理
累计长度达到0.50 m生成正常候选节点
任一积分点碰撞原语无效,立即终止
越界或进入未知区域原语无效
中途满足目标规则提前终止并保存实际有效段
Reeds–Shepp连接成功结束搜索
+ +
+TODO:后续根据障碍物距离、目标距离和窄通道自动调整原语长度;碰撞积分步长仍保持0.05 m。 +
+
+ +
+

7碰撞检测与航向栅格模板 已确认

+

7.1 两级碰撞检测

+
    +
  1. 距离场快速放行:若车辆中心到最近障碍物距离大于扩大车体外接圆半径及离散补偿,则该点必然安全;
  2. +
  3. 旋转矩形模板精确检测:距离不足以保证安全时,检查当前航向下扩大车体覆盖的所有栅格。
  4. +
+ +

7.2 外接圆仅用于“安全放行”

+
R_outer = 0.5 × sqrt(CheckLength² + CheckWidth²)
+

D > R_outer表示安全;D ≤ R_outer只表示“无法确定”,不等于碰撞,必须进入矩形模板检查。

+ +

7.3 航向栅格模板的准备

+

航向分辨率5°时,在初始化阶段自动生成72组模板:

+
0°, 5°, 10°, ... , 355°
+

每个模板保存旋转后的扩大车体矩形与地图栅格相交所对应的相对栅格偏移:

+
GridOffset { Dx, Dy }
+

运行时把当前车辆中心转换为栅格坐标,选择最近航向模板,将模板中的相对偏移平移到当前位置并查询占据状态。

+ +

7.4 模板生成规则

+
    +
  • 模板以车体几何中心为原点;
  • +
  • 车体尺寸包含安全余量与少量离散补偿;
  • +
  • 只要栅格方块与旋转矩形有交集,就加入模板;
  • +
  • 不能只判断栅格中心是否在矩形内部,否则可能漏掉边角碰撞;
  • +
  • 真实车体始终是矩形,模板呈阶梯状只是栅格化结果。
  • +
+ +

7.5 每个积分点的处理

+
读取距离场
+  ↓
+可安全放行?——是→继续
+  ↓否
+选择航向模板
+  ↓
+平移模板并查询Occupied / Unknown / 越界
+  ↓
+任一命中→碰撞;全部通过→安全
+
+ +
+

8搜索代价与启发函数 已确认

+

8.1 节点排序

+
f(n) = g(n) + w_h × h(n)
+

第一版HeuristicWeight = 1.3

+ +

8.2 累计代价

+
g_new =
+g_parent
++ C_length
++ C_reverse
++ C_switch
++ C_curvature
++ C_curvatureChange
++ C_obstacle
+
    +
  • 路径长度是基础代价;
  • +
  • 后退系数初值ReversePenalty = 1.3
  • +
  • 换向固定惩罚初值GearSwitchPenalty = 2.0
  • +
  • 曲率代价轻度抑制长期极限转向;
  • +
  • 曲率变化代价偏好平缓动作序列;
  • +
  • 障碍物代价让路径尽量远离障碍物。
  • +
+ +

8.3 双启发

+
h = max(h_2D, h_RS)
+
    +
  • h_2D:从目标反向运行二维Dijkstra得到的绕障距离;
  • +
  • h_RS:基于当前位姿、目标位姿和最小转弯半径的Reeds–Shepp距离。
  • +
+

所有权重必须外部可配置。

+
+ +
+

9终点判定、SQP精修与Reeds–Shepp 已确认

+

9.1 基本终点容差

+
GoalPositionTolerance = 0.15 m
+GoalHeadingTolerance  = 5°
+

在每个0.05 m内部积分点检查位置和航向误差,避免越过目标。

+ +

9.2 容差到达与SQP

+

Hybrid A*可以在容差范围内结束,由后续SQP通过终端约束精确收敛到目标。但必须满足:

+
    +
  • 粗路径已包含正确绕障、倒车和换向拓扑;
  • +
  • 当前终点、目标位姿及末端局部区域无碰撞;
  • +
  • 终点附近存在足够局部调整空间;
  • +
  • SQP只负责局部修正,不负责创造新的换向或改变绕障侧。
  • +
+ +

9.3 目标进入方向

+
GoalDirectionConstraint = Any | Forward | Reverse
+

默认Any。该字段约束最后一段运动方向,不改变目标车头航向定义。

+ +

9.4 Reeds–Shepp模式

+ + + + + +
模式连接失败后的行为适用场景
Disabled不尝试,满足容差即可结束开阔终点、调试对照
Opportunistic失败继续搜索,满足容差仍可交SQP默认、普通导航
RequiredNearGoal失败不能按容差结束,必须继续找可连接节点狭窄钻入、精确泊入
+ +

9.5 自动触发

+
2–5 m:每扩展10个节点尝试一次
+≤2 m:每扩展3个节点尝试一次
+采样碰撞步长:0.05 m
+

连接路径必须检查地图边界、Unknown、扩大车体碰撞、安全净空、最大曲率、方向约束及与当前曲率的衔接。

+ +

9.6 默认策略

+
ReedsSheppMode = Opportunistic
+

无需人工每次开启,由程序自动触发。任务类型可覆盖默认模式。

+
+ +
+

10搜索回溯与原始稠密路径 已确认

+

10.1 回溯内容

+

搜索成功后从终止节点沿父节点索引回溯到起点,反转节点链,并恢复每条原语保存的0.05 m内部积分点。

+ +

10.2 不能只输出0.5 m搜索节点

+

只输出原语终点会丢失圆弧形状、碰撞检查细节和换向局部结构。因此必须复用搜索时已经计算的内部点。

+ +

10.3 路径点字段

+
x, y
+heading, unwrappedHeading
+curvature
+direction
+arcLength
+clearance
+isGearSwitchPoint
+source
+ +

10.4 特殊处理

+
    +
  • 删除相邻原语重复边界点;
  • +
  • 中途终止原语只保留真实有效积分点;
  • +
  • Reeds–Shepp连接点追加到普通搜索路径之后;
  • +
  • 机会式容差终点不允许在本步骤被“硬改”为目标点;
  • +
  • 航向同时保存归一化值和连续解缠值;
  • +
  • 累计弧长始终单调增加,方向单独保存;
  • +
  • 按换向点划分Forward/Reverse连续段。
  • +
+
+ +
+

11关键点保留与弧长重采样 已确认

+

11.1 处理目标

+

原始0.05 m稠密路径不直接全部送入平滑和SQP。先保留关键结构,再按照累计弧长重采样。

+ +

11.2 必须保留的关键点

+
    +
  • 起点与终点;
  • +
  • 所有换向点;
  • +
  • 曲率等级变化点;
  • +
  • 直线/弯道切换点;
  • +
  • Reeds–Shepp分段和连接点;
  • +
  • 障碍物附近最小净空点;
  • +
  • 必要的明显航向变化点。
  • +
+ +

11.3 分方向重采样

+

每个Forward或Reverse段独立处理,不能跨越换向点插值。

+ +

11.4 推荐间距

+
普通区域:0.10 m
+重点区域:0.05 m
+

以下区域采用0.05 m:

+
    +
  • 障碍距离小于0.50 m;
  • +
  • |κ| ≥ 0.5 κmax;
  • +
  • 目标2 m范围内;
  • +
  • 换向点前后0.30 m;
  • +
  • 曲率变化点前后0.25 m;
  • +
  • Reeds–Shepp连接段与窄通道。
  • +
+ +

11.5 重采样后复核

+

新插值点必须重新执行车体碰撞检查。最终输出点可为0.10 m,但碰撞验证内部步长仍不得大于0.05 m。

+
+ +
+

12快速曲线平滑、局部QP兜底与最终校验 已确认

+

12.1 最终采用的分层方案

+
+
按Forward/Reverse方向段拆分
+
普通路径段:三次B样条快速平滑
+
局部短转角:分段三次Bézier
+
末端短连接:五次多项式(可选)
+
0.05 m完整碰撞、净空、曲率验证
+
失败局部:安全走廊约束QP
+
局部QP仍失败:回退第十一步原始可行路径
+
+ +

12.2 为什么不默认全路径QP

+

全路径多轮QP还需要走廊生成、曲率线性化和反复碰撞检测,可能与后续SQP功能重复并增加耗时。因此第一版采用快速曲线优先,仅对失败局部使用QP。

+ +

12.3 平滑结构约束

+
    +
  • 换向点固定,不能使用一条曲线跨越换向;
  • +
  • 不改变绕障侧、前进/后退顺序和换向次数;
  • +
  • Reeds–Shepp精确目标点固定;
  • +
  • 容差终点不在本步骤强行修改为目标;
  • +
  • 障碍物附近控制点移动范围收紧。
  • +
+ +

12.4 典型偏移范围

+
普通区域最大偏移:0.10–0.15 m
+障碍物附近最大偏移:0.02–0.05 m
+局部QP问题区间:失败点前后各约1.0 m
+ +

12.5 航向与曲率

+
    +
  • 平滑后必须根据新几何路径重新计算切线与航向;
  • +
  • 前进段车头航向与路径切线一致;
  • +
  • 倒车段车头航向与路径切线相差π;
  • +
  • 区分几何曲率与车辆控制曲率;
  • +
  • 建议平滑后最大车辆曲率不超过0.95 κmax
  • +
+ +

12.6 最终验证

+
    +
  • 0.05 m采样完整车体碰撞检测;
  • +
  • Unknown与越界检查;
  • +
  • 安全余量检查;
  • +
  • 最大曲率与曲率变化检查;
  • +
  • 起终点、换向点、方向分段和路径连续性检查。
  • +
+
+ +
+

13性能优化阶段 整体TODO

+

第十三步不作为第一版主流程实现要求。前12步完整跑通并建立正确性基线后,再进行性能剖析与针对性优化。

+ +

13.1 第一版仅保留防失控措施

+
    +
  • 较宽松的搜索超时;
  • +
  • 最大扩展节点和最大生成节点限制;
  • +
  • 取消请求机制;
  • +
  • 各阶段基础耗时与节点数量统计。
  • +
+ +

13.2 后续性能TODO

+
    +
  1. 统计地图处理、Dijkstra、Hybrid A*、碰撞检测、Reeds–Shepp、回溯和平滑耗时;
  2. +
  3. 根据剖析结果识别真实瓶颈;
  4. +
  5. 优化Open List、Closed Set、节点内存与对象分配;
  6. +
  7. 预计算运动原语、航向模板、距离场和启发图;
  8. +
  9. 地图裁剪、路径热启动、增量更新、多分辨率搜索;
  10. +
  11. 最终验证平均、P95、最坏时间和100 ms达成率。
  12. +
+ +
+开发顺序:正确性 → 可行性 → 可复现性 → 性能剖析 → 针对性优化 → 100 ms验收。 +
+
+ +
+

14输入输出接口、错误状态与验收 已确认

+

14.1 推荐模块

+
HybridAStarPlanner
+├── MapProcessor
+├── VehicleModel
+├── MotionPrimitiveGenerator
+├── CollisionChecker
+├── HeuristicProvider
+├── ReedsSheppConnector
+├── GoalChecker
+├── PathBacktracker
+├── PathResampler
+├── PathSmoother
+├── PathValidator
+└── PlanningDiagnostics
+ +

14.2 主接口

+
public interface IHybridAStarPlanner
+{
+    PlanningResult Plan(PlanningRequest request);
+}
+ +

14.3 输入

+
    +
  • 二维占据栅格;
  • +
  • 起点位姿与可选起始方向、起始曲率;
  • +
  • 目标位姿与目标进入方向;
  • +
  • 车辆尺寸、安全余量、最大曲率/最小转弯半径;
  • +
  • Hybrid A*配置、Reeds–Shepp模式和平滑配置。
  • +
+ +

14.4 输出

+

输出不带时间的空间路径点和分段信息:

+
X, Y
+Heading, UnwrappedHeading
+ArcLength
+GeometricCurvature
+VehicleCurvature
+Direction
+BodyClearance
+IsGearSwitchPoint
+Source
+ +

14.5 终点到达类型

+
ExactByReedsShepp
+ReachedWithinTolerance
+ExactAfterDirectSearch
+ +

14.6 主要返回状态

+
Success
+SuccessWithToleranceGoal
+SuccessWithSmoothingFallback
+InvalidRequest / InvalidMap / InvalidVehicleParameters
+StartOutsideMap / StartInCollision / StartInUnknownArea
+GoalOutsideMap / GoalInCollision / GoalInUnknownArea
+InvalidCurvatureConfiguration
+SearchTimeout / SearchNodeLimitExceeded / NoFeasiblePath
+ReedsSheppRequiredButFailed
+BacktrackingFailed / ResamplingFailed / FinalValidationFailed
+Cancelled / InternalError
+ +

14.7 第一版验收重点

+
    +
  • 有效场景能输出完整路径;
  • +
  • 起点一致,终点满足第九步规则;
  • +
  • 完整车体全程无碰撞且不进入Unknown;
  • +
  • 最大曲率不超过车辆上限;
  • +
  • 前进、倒车和换向结构正确;
  • +
  • 回溯无断点和异常重复;
  • +
  • 平滑失败可以回退原路径;
  • +
  • 失败场景返回明确状态;
  • +
  • 后续SQP能够读取并收敛。
  • +
+
+ +
+

附录A:第一版推荐配置参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
参数推荐初值说明
MapResolution0.05 m读取地图配置,不写死
HeadingResolution72个航向模板
SafetyMargin0.03 m主要加在车体矩形
PrimitiveLength0.50 m第一版固定
IntegrationStep0.05 m碰撞与积分步长
CurvatureLevels-1,-0.5,0,0.5,1 × κmax5级
GoalPositionTolerance0.15 mHybrid A*基础容差
GoalHeadingToleranceHybrid A*基础容差
HeuristicWeight1.3Weighted A*
ReversePenalty1.3允许倒车但略微惩罚
GearSwitchPenalty2.0减少频繁换向
ReedsSheppModeOpportunistic默认自动机会式
AnalyticExpansionDistance5.0 m进入后周期尝试
NearGoalDistance2.0 m提高尝试频率
AnalyticExpansionInterval10节点2–5 m
NearGoalInterval3节点≤2 m
NormalResampleSpacing0.10 m普通区域
FineResampleSpacing0.05 m重点区域
FineObstacleDistance0.50 m小于此值加密
FineGoalDistance2.0 m目标附近加密
GearSwitchDenseRange±0.30 m换向点附近
CurvatureTransitionDenseRange±0.25 m曲率切换附近
CurvatureLimitRatio0.95平滑后留控制余量
SearchTimeLimitMs3000–5000 ms第一版防失控,不是性能指标
+
+ +
+

附录B:总体伪代码

+
PlanningResult Plan(request)
+{
+    ValidateRequest(request);
+    ValidateMap(request.Map);
+    ResolveVehicleCurvatureLimit(request.Vehicle);
+    ValidateStartAndGoal();
+
+    PrepareDistanceFieldIfNeeded();
+    PrepareHeadingFootprintTemplatesIfNeeded();
+    BuildGoalDijkstraHeuristic();
+
+    startNode = CreateStartNode();
+    PushOpen(startNode);
+
+    while (OpenList not empty)
+    {
+        CheckCancellationAndSafetyLimits();
+
+        current = PopBestValidNode();
+
+        if (ShouldTryReedsShepp(current))
+        {
+            rsPath = TryReedsShepp(current, goal);
+            if (ValidateAnalyticPath(rsPath))
+                return BuildFinalResult(current, rsPath);
+        }
+
+        if (GoalChecker.CanTerminateByTolerance(current))
+            return BuildFinalResult(current, noAnalyticPath);
+
+        foreach (primitive in GenerateAllowedPrimitives(current))
+        {
+            integrated = IntegratePrimitive(
+                current,
+                primitive,
+                step = 0.05 m,
+                maxLength = 0.50 m);
+
+            if (!integrated.Valid)
+                continue;
+
+            child = CreateChildNode(integrated);
+            if (!ImproveBestCost(child))
+                continue;
+
+            PushOpen(child);
+        }
+    }
+
+    return Failure(NoFeasiblePath);
+}
+
+BuildFinalResult(goalNode, analyticPath)
+{
+    rawDense = BacktrackAndRestorePrimitivePoints(goalNode);
+    AppendAnalyticPathIfAny(rawDense, analyticPath);
+
+    segmented = SplitByMotionDirection(rawDense);
+    resampled = PreserveKeyPointsAndResample(segmented);
+
+    smoothed = TryFastSplineSmoothing(resampled);
+    if (!ValidatePath(smoothed))
+        smoothed = TryLocalCurveOrQPFallback(resampled);
+
+    if (!ValidatePath(smoothed))
+        smoothed = resampled;
+
+    if (!ValidatePath(smoothed))
+        return Failure(FinalValidationFailed);
+
+    return Success(smoothed, diagnostics);
+}
+
+ +
+

附录C:建议开发里程碑

+ + + + + + + + + + + +
阶段范围完成标准
M0 数据与工具Pose、地图、车辆参数、角度/坐标工具单元测试通过
M1 运动学与碰撞恒曲率积分、航向模板、矩形碰撞可视化验证不同角度无漏检
M2 最小Hybrid A*无障碍、只前进、基础启发稳定从起点到终点
M3 障碍与倒车Dijkstra、前进/倒车、换向代价绕障与一次倒车场景通过
M4 终点模块容差、GoalDirection、Reeds–Shepp三模式普通与狭窄目标场景通过
M5 后处理回溯、0.05 m稠密点、分段、重采样路径结构无断点、换向明确
M6 平滑与复核B样条、Bézier、局部QP兜底不改变拓扑,失败可回退
M7 接口与集成PlanningRequest/Result、状态、诊断可接入SQP
M8 性能TODO剖析、优化、100 ms目标在正确性基线后执行
+
+
+
+ +
+ 文档用途:四舵轮AMR第一阶段非结构化道路粗路径规划开发依据。内部单位统一采用米、弧度、1/米。 +
+ + diff --git a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md new file mode 100644 index 0000000..9ee4f13 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md @@ -0,0 +1,726 @@ +# Hybrid A* 粗路径规划实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 + +**目标:** 在 ClumsyPilot `netstandard2.0` 项目中交付可复用的静态规划地图和 Hybrid A* 粗路径规划模块,通过一次 `CoarsePathPlanningService.Plan(job)` 完成建图、快照复用、粗路径搜索和可选调试发布。 + +**架构:** `Map` 模块统一接收人工障碍、TwoLeg 及未来障碍来源,生成只含外部障碍物的不可变 `PlanningGridMap`;`HybridAStarPlanner` 只消费该快照并完成保守碰撞检查与搜索;`CoarsePathPlanningService` 负责编排。核心逻辑不读取传感器、UI 或系统时间,Clumsy `MovementTest` 和 PNG 导出只作为旁路适配器。 + +**技术栈:** C# 10、.NET Standard 2.0、PowerShell 反射契约测试、Clumsy `MovementTest`、StbImageWriteSharp 1.16.7。 + +**设计依据:** `docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md` + +## 全局约束 + +- 所有命令均从仓库根目录执行;项目文件为 `ClumsyPilot/ClumsyPilot.csproj`。 +- 目标框架保持 `netstandard2.0`,不得直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite` 或依赖 `record/init` 的实现。 +- 地图边界、人工障碍和 TwoLeg 快照使用世界坐标 mm;规划内部统一使用 m、rad、1/m。 +- 地图只保存外部障碍物,不写入 AMR 自身足迹,不在地图侧添加车辆安全膨胀。 +- 安全余量只在碰撞检查时扩大车体矩形,防止地图膨胀与车体膨胀重复计算。 +- `ResolutionMm` 必须在 20~200 mm;地图最多 4,000,000 格,分配前使用 `checked` 检查。 +- 地图坐标采用 `[XMin, XMax) × [YMin, YMax)`;地图外始终按占据处理。 +- `PrimitiveLengthMeters=0.50` 表示原语最大长度;`IntegrationStepMeters=0.05` 表示积分最大步长。 +- 碰撞采样中心位移不得超过 `min(0.025 m, Map.ResolutionMeters / 2)`。 +- 默认终点容差为 0.15 m 和 5°。每个原语在内部积分点逐点检查目标,第一次满足条件即截断为终点候选。 +- 生成终点候选时不能立即成功;候选必须进入 Open List,作为当前最佳有效条目出队时才成功。 +- 第一版只支持恒曲率前进、倒车及原语边界换向;不支持蟹行、横移、原地旋转、Reeds-Shepp 精确连接、平滑、速度规划或底盘控制。 +- PowerShell 脚本首行设置 `$ErrorActionPreference = 'Stop'`,统一加载 `bin/Debug/netstandard2.0/ClumsyPilot.dll`。 +- 用户已明确不需要 Git 自检。本计划不包含 `git diff`、`git add`、`git commit` 等步骤;版本管理由用户另行处理。 + +## P0 与 P1 完成定义 + +| 阶段 | 必须完成的结果 | 阶段出口 | +| --- | --- | --- | +| P0-MAP:当前首要工作 | Map 文件结构、统一障碍来源、TwoLeg 投影、栅格化、距离场、快照缓存、PNG 迁移、自动测试和 `MovementTest.MapTest` 实际调用 | Map 独立构建成功;3 个 Map 脚本通过;Clumsy MapTest 只通过 `PlanningMapFactory.Create` 完成建图与显示 | +| P0-PLAN:Map Gate 之后 | CoarsePath 契约、保守碰撞、原语截断、Open List、Hybrid A*、输出校验和一次调用门面 | 6 个非 UI P0 脚本全部通过;固定场景可由 `CoarsePathPlanningService` 返回经最终复核的粗路径 | +| P1:集成与优化 | CoarsePath MovementTest、性能基准、旧地图退役和 README | 7 个功能脚本及 Release 基准通过;UI 入口只做调试展示;旧地图类型不再成为规划运行时入口 | + +执行顺序固定为“P0-MAP 独立闭环 → P0-PLAN → P1”。除了 Map 自己的 `MovementTest.MapTest` 和 PNG 调试辅助外,在 Map Gate 通过前不编写粗路径搜索或 CoarsePath UI,避免旧 `MovementTest.Trapmaptest.cs` 的传感器和渲染依赖进入核心模块。 + +## 当前首要里程碑:P0-MAP 独立闭环 + +当前阶段只处理 `Utils` 中被 Map 使用的无状态工具、完整 `Map` 目录、Map 自动化测试和 Map 实际调用。不得提前创建 `CoarsePath/Search`、`CoarsePath/Vehicle` 或 `CoarsePath/Output` 运行时代码。 + +### Map 实施顺序 + +1. 建立 `Map/Core`、`Map/Obstacles`、`Map/Sources`、`Map/Planning`、`Map/Test/Visualization` 目录和命名空间。 +2. 完成 `MapBoundsMm`、连续行优先 `EnvironmentGridMap`、圆/矩形 DTO 和唯一栅格化器。 +3. 完成统一 `IMapObstacleSource`、人工来源、TwoLeg 快照来源和事务式 `EnvironmentMapBuilder`。 +4. 完成 `PlanningGridMap`、精确 EDT、保守距离场和 mm→m 适配。 +5. 完成 `PlanningMapFactory`、容量 4 的两级快照缓存和地图变化判断。 +6. 将旧 `TrapMapImageExporter.cs` 能力拆到 `Map/Test/Visualization`,只消费只读快照。 +7. 完成三个 Map PowerShell 脚本和 `MovementTest.MapTest`,用真实入口验证建图、复用和显示。 + +对应详细任务的执行次序为:`Task 1 → Task 3 → Task 4 → Task 5 → Task 6 → Task 13 → Task 14 的 MapTest 部分 → Map Gate`。`Task 2` 和 `Task 7~12` 在 Map Gate 之后执行。 + +### Map 实际调用契约 + +`PlanningMapFactory` 应由长期存在的服务或测试对象持有,不能每次调用都重新 `new`,否则容量 4 的快照缓存无法跨调用复用: + +```csharp +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + +public PlanningMapBuildResult CreateCurrentMap( + MapBoundsMm bounds, + long manualVersion, + IReadOnlyList manualObstacles, + long twoLegVersion, + TwoLegProjectionInput twoLegSnapshot) +{ + return _mapFactory.Create(new PlanningMapRequest + { + Bounds = bounds, + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource( + "manual", manualVersion, true, manualObstacles), + new TwoLegObstacleSource( + "two-leg", twoLegVersion, false, twoLegSnapshot), + }, + AllowExplicitEmptyMap = false, + }); +} +``` + +调用方只判断统一结果,不接触 builder、rasterizer、投影器或 adapter: + +```csharp +PlanningMapBuildResult result = CreateCurrentMap( + bounds, manualVersion, manualObstacles, twoLegVersion, twoLegSnapshot); + +if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + return; + +PlanningGridMap map = result.Map; +bool worldOriginIsOccupied = map.IsOccupiedWorld(0d, 0d); +double worldOriginClearance = + map.GetConservativeObstacleDistanceMeters(0d, 0d); +``` + +### Map Gate + +以下条件必须全部满足,才开始 CoarsePath 契约和搜索: + +- [ ] 新 `Map` 运行时代码不读取 `TwoLegDetect`、定位、Painter、Toast、UI 或系统时间。 +- [ ] 人工、TwoLeg 以及未来来源全部经 `IMapObstacleSource.ProjectToWorld()` 输出世界 mm 几何,再由唯一 rasterizer 写图。 +- [ ] 地图不写 AMR 自身,不使用旧默认 300 mm 膨胀。 +- [ ] `PlanningMapFactory.Create` 对完整输入命中、占据命中和占据变化给出可区分结果。 +- [ ] `MovementTest.MapTest` 只调用 `PlanningMapFactory` 和可选 PNG exporter,不复制地图构造逻辑。 +- [ ] 下列命令全部通过: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +``` + +### P0-MAP 执行状态(2026-07-27) + +- [x] 已完成 Task 1、Task 3~6、Task 13,以及 Task 14 的 `MovementTest.MapTest` 部分。 +- [x] 已建立新的 `Map` 目录结构、统一障碍物来源与 TwoLeg 检测时位姿投影;Map 核心不读取传感器、UI 或系统时钟。 +- [x] 已完成只读规划快照、保守 EDT 距离场、容量 4 的 LRU 缓存、PNG 可视化拆分和实际 MapTest 调用。 +- [x] 已通过上述 Map Gate 命令;另外 `verify_planning_utils.ps1` 也已通过。 +- [x] 未执行 Git 自检、暂存或提交。 + +## 最终目录与文件职责 + +```text +ClumsyPilot/ParkrobTrajplanner/ +├── Initial_plan/ ------ 方案与实施文档,不放运行时代码 +├── Utils/ ------ 通用、无状态、确定性数值工具 +│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引 +│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率转换 +│ ├── CoordinateTransform.cs ------ 车体坐标与世界坐标二维刚体变换 +│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验 +│ └── GridIndex.cs ------ 不可变行列索引值对象 +├── Map/ ------ MultiWheelC.TrajectoryPlanning.Mapping +│ ├── Core/ +│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 占据栅格 +│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界 +│ │ ├── MapBuildRequest.cs ------ 内部环境图构建请求 +│ │ ├── EnvironmentMapBuildResult.cs ------ 环境图、来源摘要和失败原因 +│ │ └── EnvironmentMapBuilder.cs ------ 校验来源并事务式合并障碍图层 +│ ├── Obstacles/ +│ │ ├── IMapObstacle.cs ------ 世界坐标障碍物公共几何契约 +│ │ ├── AxisAlignedRectangleObstacle.cs ------ 轴对齐矩形障碍 DTO +│ │ ├── CircleObstacle.cs ------ 圆形障碍 DTO +│ │ └── MapObstacleRasterizer.cs ------ 唯一占据栅格写入器 +│ ├── Sources/ +│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一接口 +│ │ ├── ObstacleSourceStatus.cs ------ Applied/Empty/Unavailable/Invalid +│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和障碍集合 +│ │ ├── ManualObstacleSource.cs ------ 输出人工圆和矩形 +│ │ ├── TwoLegProjectionInput.cs ------ 两腿端点、检测状态和检测位姿快照 +│ │ ├── TwoLegObstacleSource.cs ------ TwoLeg 统一来源适配器 +│ │ └── TwoLegObstacleProjector.cs ------ 车体系两腿端点投影到世界系 +│ ├── Planning/ +│ │ ├── PlanningGridMap.cs ------ 只读 m 占据图、距离场和快照元数据 +│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间精确二维欧氏距离变换 +│ │ ├── ObstacleDistanceField.cs ------ 不高估真实净空的距离下界 +│ │ ├── PlanningMapCache.cs ------ 容量 4 的线程安全两级快照缓存 +│ │ └── PlanningMapAdapter.cs ------ 占据深拷贝、mm→m 和距离场生成 +│ ├── PlanningMapRequest.cs ------ Map 模块统一输入 +│ ├── PlanningMapBuildResult.cs ------ Map 模块统一输出和缓存命中类型 +│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共创建入口 +│ └── Test/ +│ ├── MovementTest.MapTest.cs ------ Clumsy UI 地图构建/显示入口 +│ └── Visualization/ +│ ├── PlanningMapImageExportRequest.cs ------ 快照、叠加层和输出选项 +│ ├── PlanningMapImageExportResult.cs ------ PNG 状态、路径、尺寸和诊断 +│ ├── PlanningMapImageExporter.cs ------ 校验、渲染编排和原子发布 +│ ├── PlanningMapImageRenderer.cs ------ 地图/车辆/路径绘制到 RGBA +│ └── ValidatedPngWriter.cs ------ Stb 编码、PNG 结构和 CRC 校验 +├── CoarsePath/ ------ MultiWheelC.TrajectoryPlanning.CoarsePath +│ ├── Contracts/ +│ │ ├── Pose2D.cs ------ m/rad 不可变二维位姿 +│ │ ├── TravelDirection.cs ------ Forward/Reverse +│ │ ├── GoalDirectionConstraint.cs ------ Any/Forward/Reverse +│ │ ├── VehicleParameters.cs ------ 尺寸、安全余量和最大曲率 +│ │ ├── PlanningRequest.cs ------ 纯规划器输入 +│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差 +│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和分段 +│ │ ├── PlanningStatus.cs ------ 输入、碰撞、搜索和验证状态 +│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计 +│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和净空 +│ │ ├── CoarsePathPointSource.cs ------ Start/MotionPrimitive/GoalTruncation +│ │ └── PathSegment.cs ------ 包含式方向分段索引 +│ ├── Vehicle/ +│ │ ├── VehicleKinematics.cs ------ 解析保守最大曲率 +│ │ ├── VehicleFootprint.cs ------ 扩大矩形、AABB 和外接圆 +│ │ ├── OrientedRectangleCellIntersection.cs ------ 旋转矩形与格矩形精确相交 +│ │ └── FootprintCollisionChecker.cs ------ 边界、快速放行、精确和扫掠检查 +│ ├── Search/ +│ │ ├── MotionPrimitive.cs ------ 恒曲率原语和实际截断长度 +│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分并保留内部采样点 +│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 确定性 Open List +│ │ ├── SearchCostCalculator.cs ------ 统一计算等效米搜索代价 +│ │ ├── HybridAStarNode.cs ------ 连续状态、代价、父索引和原语 +│ │ ├── HybridAStarNodeKey.cs ------ 位置/航向/方向/曲率离散键 +│ │ ├── GoalToleranceChecker.cs ------ 位置、航向和进入方向判断 +│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域、禁止切角的二维启发 +│ │ └── HybridAStarSearch.cs ------ 扩展、重开、限额和候选管理 +│ ├── Output/ +│ │ ├── PathBacktracker.cs ------ 根据父索引重建原语内部点 +│ │ ├── CoarsePathAssembler.cs ------ 弧长、换向点和方向分段 +│ │ └── CoarsePathValidator.cs ------ 数值、碰撞、曲率、终点和分段复核 +│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的下层门面 +│ ├── Facade/ +│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需全部输入 +│ │ ├── CoarsePathPlanningJobResult.cs ------ 地图结果与粗路径结果 +│ │ ├── PlanningDebugOptions.cs ------ 地图、路径、碰撞调试开关 +│ │ ├── IPlanningDebugSink.cs ------ 不改变规划状态的调试消费接口 +│ │ └── CoarsePathPlanningService.cs ------ 建图、搜索和调试的一次调用入口 +│ └── Test/ +│ ├── CoarsePathScenarioFactory.cs ------ 固定、可复现的地图与规划场景 +│ └── MovementTest.CoarsePathTest.cs ------ 后台规划并在 Clumsy UI 显示 +│ └── README.md ------ 粗规划模块边界、调用示例和文档链接 + +ClumsyPilot/tests/ +├── verify_planning_utils.ps1 ------ 工具与公共契约 +├── verify_planning_map_factory.ps1 ------ 来源、事务、缓存与快照 +├── verify_planning_map_adapter.ps1 ------ 栅格、适配和距离场 +├── verify_planning_map_image.ps1 ------ PNG 渲染与原子发布 +├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞 +├── verify_coarse_path_search.ps1 ------ 原语截断、堆、代价和搜索 +├── verify_coarse_path_integration.ps1 ------ 一次调用端到端验证 +└── benchmark_coarse_path.ps1 ------ Release 性能与内存门槛 +``` + +## 固定公共调用方式 + +常规业务代码只保留一个长期存在的服务实例: + +```csharp +var result = planningService.Plan(new CoarsePathPlanningJob +{ + MapRequest = new PlanningMapRequest + { + Bounds = bounds, + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource( + "manual", manualVersion, true, manualObstacles), + new TwoLegObstacleSource("two-leg", twoLegVersion, false, twoLegSnapshot), + }, + AllowExplicitEmptyMap = true, + }, + Start = startPose, + Goal = goalPose, + Vehicle = vehicle, + Configuration = configuration, + StartVehicleCurvature = 0d, + StartDirection = null, + GoalDirection = GoalDirectionConstraint.Any, + Debug = new PlanningDebugOptions + { + VisualizeMap = true, + VisualizePath = true, + VisualizeCollisionChecks = false, + }, +}, cancellationToken); +``` + +已有地图快照复用或纯搜索测试才直接调用 `HybridAStarPlanner.Plan(PlanningRequest, CancellationToken)`。业务调用方不得直接拼装 builder、rasterizer、碰撞器、运动原语或搜索节点。 + +## P0:正确可用 + +### Task 1:建立 Utils 与测试基座 + +**文件:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs` +- Create: `ClumsyPilot/tests/verify_planning_utils.ps1` + +**产出接口:** + +```csharp +double AngleMath.NormalizeRadians(double radians); +double AngleMath.ShortestSignedDifference(double from, double to); +int AngleMath.ToHeadingIndex(double heading, double resolution, int binCount); +double UnitConverter.MillimetersToMeters(double value); +double UnitConverter.DegreesToRadians(double value); +bool NumericGuard.IsFinite(double value); +readonly struct GridIndex { int Row; int Col; } +``` + +- [ ] 写失败测试:断言 `2π→0`、`179°→-179°` 最短角差为 `2°`、`1250 mm→1.25 m`、车体系 `(1000,0)` 在世界位姿 `(2000,3000,90°)` 后得到 `(2,4) m`,并拒绝 NaN/Infinity。 +- [ ] 执行以下命令,预期构建成功、脚本因新类型不存在返回非零。 + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +``` + +- [ ] 实现:角度统一到 `[-π,π)`;航向索引先归一化再 `floor`;刚体变换使用 `world=origin+R(heading)×local`;`GridIndex` 实现值相等和稳定哈希。 +- [ ] 重跑脚本,预期输出 `Planning utility checks passed.`。 + +### Task 2:锁定 CoarsePath 公共契约与默认参数 + +**文件:** + +- Create: `CoarsePath/Contracts/` 下目录树列出的 12 个契约文件 +- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` + +**固定默认值:** + +```csharp +PrimitiveLengthMeters = 0.50; +IntegrationStepMeters = 0.05; +MaximumCollisionCheckStepMeters = 0.025; +HeadingResolutionRadians = Math.PI / 36d; +CurvatureLevelCount = 5; +GoalPositionToleranceMeters = 0.15; +GoalHeadingToleranceRadians = Math.PI / 36d; +MaximumExpandedNodes = 200000; +SearchTimeout = TimeSpan.FromSeconds(5); +HeuristicWeight = 1.0; +ReverseCostMultiplier = 1.5; +GearSwitchPenaltyMeters = 1.0; +CurvatureMagnitudeWeight = 0.10; +CurvatureChangePenaltyMetersPerLevel = 0.05; +ClearanceCostWeight = 0.20; +ClearanceCostDistanceMeters = 0.50; +``` + +- [ ] 扩展失败测试:断言上述默认值,三个方向/来源枚举,以及 `PlanningRequest` 的起始曲率、起始方向、目标进入方向。 +- [ ] 断言 `PlanningStatus` 包含:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 +- [ ] 实现不可变 `Pose2D`;结果工厂只允许成功结果携带路径,失败结果路径为空。 +- [ ] `PlanningDiagnostics` 固定记录扩展节点数、生成节点数、重新打开节点数、陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因。 +- [ ] `CoarsePathPoint` 固定包含 `X/Y`、`Heading/UnwrappedHeading`、`ArcLength`、`Direction`、`VehicleCurvature`、`BodyClearance`、`IsGearSwitchPoint` 和 `Source`;`PathSegment` 固定包含方向和包含式 `StartIndex/EndIndex`。 +- [ ] 重跑工具脚本,预期契约和默认值全部通过。 + +### Task 3:实现环境栅格、边界与统一栅格化 + +**文件:** + +- Create: `Map/Core/MapBoundsMm.cs` +- Create: `Map/Core/EnvironmentGridMap.cs` +- Create: `Map/Obstacles/IMapObstacle.cs` +- Create: `Map/Obstacles/AxisAlignedRectangleObstacle.cs` +- Create: `Map/Obstacles/CircleObstacle.cs` +- Create: `Map/Obstacles/MapObstacleRasterizer.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` + +上述 Map/CoarsePath 相对路径均位于 `ClumsyPilot/ParkrobTrajplanner/`。 + +- [ ] 写边界和栅格失败测试:20/200 mm 合法,范围外失败;4,000,001 格分配前失败;`XMax/YMax` 排他;非完整末格裁剪;地图外占据;圆/矩形与格边或格角接触时保守占据;完全在地图外的合法障碍不写格。 +- [ ] 运行脚本,预期新 Map 类型不存在。 +- [ ] 实现私有行优先 `byte[]`,索引固定为 `row*Cols+col`;世界转格使用 `floor((value-min)/resolution)`;不得返回内部缓冲区。 +- [ ] 实现唯一栅格化器:圆使用“圆心到格矩形最近点距离”,轴对齐矩形使用闭区间相交,只遍历裁剪后的候选包围盒。 +- [ ] 确认不存在 `MarkVehicleFootprint` 或安全距离参数,重跑脚本通过。 + +### Task 4:统一人工与 TwoLeg 障碍来源 + +**文件:** + +- Create: `Map/Sources/` 下目录树列出的 7 个文件 +- Create: `Map/Core/MapBuildRequest.cs` +- Create: `Map/Core/EnvironmentMapBuildResult.cs` +- Create: `Map/Core/EnvironmentMapBuilder.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_factory.ps1` + +**固定接口:** + +```csharp +public interface IMapObstacleSource +{ + string SourceId { get; } + long SourceVersion { get; } + bool IsRequired { get; } + ObstacleProjectionResult ProjectToWorld(); +} +``` + +- [ ] 构造函数统一为 `ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles)` 和 `TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input)`;计划内所有实际调用均使用这两个签名。 +- [ ] 写来源事务失败测试:人工 `Applied/Empty`、TwoLeg 两个圆、可选来源 `Unavailable/Invalid` 保留人工图层、必需来源失败导致整图失败、重复 `SourceId` 失败、输入顺序不同但结果相同。 +- [ ] 校验 `SourceId` 非空且一次请求内唯一,`SourceVersion` 非负;来源快照内容发生变化时,上层必须递增版本。 +- [ ] 写 TwoLeg 坐标测试:使用检测时车辆位姿完成车体系 mm 到世界系 mm 变换,不得使用规划开始时位姿。 +- [ ] 实现纯快照投影:`ProjectToWorld()` 不能调用 `TwoLegDetect`、定位、UI 或系统时间;过期判断由上层采集适配器在构造 DTO 前完成。 +- [ ] builder 按 `SourceId` 排序;可选失败只记诊断;必需失败不发布地图;全部成功几何交给唯一 rasterizer。 +- [ ] 重跑工厂脚本,预期来源状态、投影和事务断言通过。 + +### Task 5:生成不可变 PlanningGridMap 和保守距离场 + +**文件:** + +- Create: `Map/Planning/EuclideanDistanceTransform.cs` +- Create: `Map/Planning/ObstacleDistanceField.cs` +- Create: `Map/Planning/PlanningGridMap.cs` +- Create: `Map/Planning/PlanningMapAdapter.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` + +- [ ] 扩展失败测试:mm→m、占据深拷贝、地图外距离为零、障碍格距离为零、空图距离正无穷、末格裁剪,以及所有样本距离不高于暴力几何距离。 +- [ ] 实现两次一维平方距离变换,复杂度 `O(Rows×Cols)`;不得逐自由格遍历全部障碍格。 +- [ ] 对精确栅格中心距离应用: + +```csharp +Math.Max(0d, centerDistanceMeters - Math.Sqrt(2d) * resolutionMeters) +``` + +- [ ] 查询使用点所在格的保守值,不做可能抬高结果的插值;空图跳过 EDT;边界检查独立执行。 +- [ ] `PlanningGridMap` 私有保存连续占据/距离数组,不提供写入口和缓冲区引用。 +- [ ] 重跑地图适配脚本,预期栅格、深拷贝、空图和保守距离全部通过。 + +### Task 6:实现 PlanningMapFactory 与两级快照缓存 + +**文件:** + +- Create: `Map/Planning/PlanningMapCache.cs` +- Create: `Map/PlanningMapRequest.cs` +- Create: `Map/PlanningMapBuildResult.cs` +- Create: `Map/PlanningMapFactory.cs` +- Modify: `Map/Planning/PlanningGridMap.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` + +```csharp +public sealed class PlanningMapFactory +{ + public PlanningMapBuildResult Create(PlanningMapRequest request); +} +``` + +`PlanningMapRequest` 固定包含 `MapBoundsMm Bounds`、`float ResolutionMm`、`IReadOnlyList ObstacleSources`、`bool AllowExplicitEmptyMap`。 + +- [ ] 写缓存测试:完全相同请求返回同一 `PlanningGridMap`;来源版本变化但最终占据未变化时产生新 `SnapshotId` 并复用占据/距离缓冲;占据变化时重建距离场。 +- [ ] 写规划可用性测试:至少一个成功来源提供有效障碍语义时 `PlanningReady=true`;所有来源均为空时仅 `AllowExplicitEmptyMap=true` 可用;必需来源失败或未明确空图语义时 `PlanningReady=false` 并填写 `PlanningBlockReason`,规划器随后返回 `MapNotReady`。 +- [ ] 验证失效边界:起终点、车辆、规划参数和可视化开关不进入地图指纹;边界、分辨率、空图策略、来源状态/版本或规范化几何变化必须进入。 +- [ ] 实现确定性 `InputFingerprint`:来源先按 ID 排序,字段固定顺序,浮点按 IEEE 位模式;命中后仍比较规范化结构。 +- [ ] 栅格化后计算 `OccupancyHash`;命中后仍比较地图几何、数组长度和逐字节内容,不能只信任哈希。 +- [ ] 实现容量 4 的线程安全 LRU;完整命中返回同一快照,占据命中共享不可变缓冲但生成新元数据,只有占据变化才运行 EDT。 +- [ ] `PlanningMapBuildResult` 固定返回 `Succeeded`、失败原因、来源摘要、缓存命中类型和成功时的 `PlanningGridMap`;快照固定保存 `PlanningReady`、`PlanningBlockReason`、来源版本摘要、`InputFingerprint`、`OccupancyHash`、单调 `SnapshotId`。 +- [ ] 增加 16 个并发相同请求测试和 LRU 淘汰测试,重跑脚本通过。 + +### Task 7:实现连续车体足迹和保守碰撞检查 + +**文件:** + +- Create: `CoarsePath/Vehicle/` 下目录树列出的 4 个文件 +- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` + +**固定接口:** + +```csharp +bool IsPoseCollisionFree( + Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, + double additionalMarginMeters, out double bodyClearanceMeters); + +bool IsSweptMotionCollisionFree( + Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, + double maximumCenterStepMeters, out double minimumBodyClearanceMeters); +``` + +- [ ] 写失败测试:正交、45°、任意航向、栅格中心/亚栅格中心、边角接触、薄障碍、地图边界、距离场快速放行,以及两个无碰撞端点之间有障碍的扫掠场景。 +- [ ] 实现以几何中心为参考的扩大车体;安全余量加到长度和宽度两侧;最大曲率与最小转弯半径并存时取更保守限制。 +- [ ] 使用分离轴定理精确判断连续旋转矩形与占据格矩形相交,接触视为碰撞;不得使用离散航向模板。 +- [ ] 检查顺序:扩大车体边界 → 保守距离严格大于外接圆时快速放行 → AABB 内占据格 SAT。 +- [ ] 扫掠采样中心步长不超过 `min(configuredStep,map.Resolution/2)`;每段临时附加余量为: + +```text +0.5 × (centerDisplacement + + circumscribedRadius × abs(headingDelta)) +``` + +- [ ] 重跑碰撞脚本,预期所有亚栅格、擦边和扫掠案例通过。 + +### Task 8:实现解析恒曲率原语和内部终点截断 + +**文件:** + +- Create: `CoarsePath/Search/MotionPrimitive.cs` +- Create: `CoarsePath/Search/MotionPrimitiveGenerator.cs` +- Create: `CoarsePath/Search/GoalToleranceChecker.cs` +- Create/Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**解析积分:** + +```csharp +double signedDistance = direction == TravelDirection.Forward ? step : -step; +double nextHeading = AngleMath.NormalizeRadians( + heading + curvature * signedDistance); + +if (Math.Abs(curvature) < 1e-12) +{ + nextX = x + signedDistance * Math.Cos(heading); + nextY = y + signedDistance * Math.Sin(heading); +} +else +{ + nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; + nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; +} +``` + +- [ ] 写原语测试:直行、圆弧、倒车、五级曲率、相邻曲率最多变化一级、最大长度 0.50 m、实际采样步长不超过 `min(IntegrationStep,CollisionStep,MapResolution/2)`。 +- [ ] 写用户提出的案例:目标距起点 0.30 m、原语最大 0.50 m、收紧容差;断言第一个满足目标的内部点截断,后续点不生成,来源为 `GoalTruncation`。 +- [ ] 每个内部点严格按“有限值 → 扫掠碰撞 → 目标条件”检查;碰撞必须先于目标。 +- [ ] 起点已满足目标时创建零长度候选,不生成原语。 +- [ ] 重跑搜索脚本,预期几何、碰撞采样和 0.30 m 截断案例通过。 + +### Task 9:实现 Open List、统一代价和二维启发 + +**文件:** + +- Create: `CoarsePath/Search/BinaryMinHeap.cs` +- Create: `CoarsePath/Search/SearchCostCalculator.cs` +- Create: `CoarsePath/Search/GridDijkstraHeuristic.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**固定代价:** + +```text +primitiveCost = + lengthMeters + × directionMultiplier + × (1 + + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature) + + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters)) + + gearSwitchPenalty + + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta) +``` + +- [ ] 写堆顺序测试:较小 `F`、较小 `H`、较大 `G`、较小插入序号;相同输入重复运行顺序一致。 +- [ ] 写代价测试:前进、倒车、换向、曲率幅值、曲率变化和净空项;拒绝负数及非有限权重。 +- [ ] 写 Dijkstra 测试:八邻域直/斜代价;两个正交邻格任一占据时禁止对角切角;二维不可达返回明确状态。 +- [ ] 实现专用二叉最小堆,不依赖 `PriorityQueue`;允许旧条目由搜索层惰性丢弃。 +- [ ] `HeuristicWeight=1` 使用 `F=G+H`;大于 1 时只承诺可行性。 +- [ ] 重跑搜索脚本,预期顺序、公式和切角限制通过。 + +### Task 10:实现 Hybrid A* 节点、重开和终点候选管理 + +**文件:** + +- Create: `CoarsePath/Search/HybridAStarNode.cs` +- Create: `CoarsePath/Search/HybridAStarNodeKey.cs` +- Create: `CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +- [ ] 写搜索测试:空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、`±π` 容差、无解、取消、超时、节点上限、重开和确定性。 +- [ ] 写候选排序测试:先生成较大 `F` 的终点候选时不得结束;较小 `F` 普通节点先出队;候选成为最佳有效条目后才成功。 +- [ ] 离散键固定为位置格、航向格、方向和曲率等级;连续位姿保留在节点中。 +- [ ] `Dictionary` 保存普通状态最佳 `G`;更小 `G` 允许重开;旧普通堆条目惰性丢弃。 +- [ ] 终点候选放入同一堆,但不能仅因另一个连续位姿落入相同离散键且 `G` 更低而被删除;候选出队时重新验证目标和末段碰撞。 +- [ ] 搜索循环在每次扩展前按顺序检查取消、5 s 超时、200,000 节点上限,再弹出有效条目;Open List 为空返回 `NoFeasiblePath`。 +- [ ] 重跑搜索脚本,预期候选顺序、重开、限额和所有场景通过。 + +### Task 11:回溯、装配、最终复核和 HybridAStarPlanner + +**文件:** + +- Create: `CoarsePath/Output/PathBacktracker.cs` +- Create: `CoarsePath/Output/CoarsePathAssembler.cs` +- Create: `CoarsePath/Output/CoarsePathValidator.cs` +- Create: `CoarsePath/HybridAStarPlanner.cs` +- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +```csharp +public sealed class HybridAStarPlanner +{ + public PlanningResult Plan( + PlanningRequest request, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] 写失败状态测试:请求、地图就绪、车辆参数、曲率配置、起终点越界/碰撞,以及搜索失败到结果的无异常映射。 +- [ ] 写输出测试:首点弧长零;弧长不递减;`UnwrappedHeading` 连续;终点截断来源正确;除换向对外无相邻重复点。 +- [ ] 写换向分段测试:换向处保留两个坐标/航向/弧长相同而方向不同的点;新方向点标记 `IsGearSwitchPoint`;包含式分段完整覆盖路径。 +- [ ] 搜索节点只存父索引、方向、曲率和实际原语长度;成功后使用相同解析积分和有效步长重建内部点。 +- [ ] 最终复核有限数值、曲率、扫掠碰撞、目标容差/方向、弧长、换向对和分段;失败返回 `FinalValidationFailed` 且不发布部分路径。 +- [ ] 重跑碰撞、搜索和集成脚本,预期全部通过。 + +### Task 12:实现一次调用 CoarsePathPlanningService + +**文件:** + +- Create: `CoarsePath/Facade/` 下目录树列出的 5 个文件 +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +```csharp +public sealed class CoarsePathPlanningService +{ + public CoarsePathPlanningJobResult Plan( + CoarsePathPlanningJob job, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] 写一次调用测试:固定执行 `PlanningMapFactory.Create → HybridAStarPlanner.Plan → debug sink`;地图失败不启动搜索;结果同时保留地图和规划结果。 +- [ ] 写旁路隔离测试:可视化开关不改变地图指纹、占据哈希、规划状态或路径;debug sink 异常只写调试诊断。 +- [ ] 服务持有同一 `PlanningMapFactory`,多次调用共享容量 4 缓存;默认 debug sink 为空行为。 +- [ ] 重跑以下 P0 验收,预期构建成功且 6 个脚本退出码为 0。 + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +## P0-MAP 收尾与 P1:集成、迁移和性能 + +### Task 13(P0-MAP):拆分迁移 TrapMapImageExporter + +**现有来源:** + +- Read/Migrate: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` + +**新文件:** + +- Create: `Map/Test/Visualization/PlanningMapImageExportRequest.cs` +- Create: `Map/Test/Visualization/PlanningMapImageExportResult.cs` +- Create: `Map/Test/Visualization/PlanningMapImageExporter.cs` +- Create: `Map/Test/Visualization/PlanningMapImageRenderer.cs` +- Create: `Map/Test/Visualization/ValidatedPngWriter.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_image.ps1` + +- [ ] 写新图片测试:输入只允许 `PlanningGridMap`;覆盖关闭导出、非法尺寸、唯一命名、临时文件清理、PNG 签名/IHDR/IEND 和每个 chunk CRC。 +- [ ] 保留现有有效限制:`PixelsPerCell=4`、`MaximumImageEdgePixels=4000`、`MaximumFileSizeBytes=50 MiB`、`OutputDpi=300`、最多 1024 次重名重试、StbImageWriteSharp 1.16.7。 +- [ ] 按职责迁移:Request/Result 只放 DTO;Renderer 生成 RGBA;Writer 负责编码/CRC;Exporter 校验、独占临时文件和原子发布。 +- [ ] 新导出器不得依赖 `GridMapData`、`TrapMapVehiclePose`、TwoLeg 状态、传感器或地图构造器;车辆、起终点和路径只作可选叠加层。 +- [ ] 运行图片脚本通过后先保留旧文件,Task 16 确认等价覆盖后再退役。 + +### Task 14:增加地图与粗路径 MovementTest + +**文件:** + +- Create: `Map/Test/MovementTest.MapTest.cs` +- Create: `CoarsePath/Test/CoarsePathScenarioFactory.cs` +- Create: `CoarsePath/Test/MovementTest.CoarsePathTest.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +- [ ] **P0-MAP 部分:** 先创建 `MovementTest.MapTest.cs`。它只调用长期持有的 `PlanningMapFactory`,显示来源状态、栅格、快照 ID 和缓存命中,并可选调用新 PNG 导出器;通过 Map Gate 后即可结束当前首要里程碑。 +- [ ] **P1 部分:** Map Gate 和 P0-PLAN 均通过后,再创建 `CoarsePathScenarioFactory.cs` 与 `MovementTest.CoarsePathTest.cs`。 +- [ ] 场景工厂提供显式空图、单矩形绕行、人工圆+矩形+TwoLeg、相同地图缓存命中、倒车换向和无解案例。 +- [ ] CoarsePath 案例使用同一 `CoarsePathPlanningService`;测试类不得直接实例化 rasterizer、碰撞器、原语生成器或搜索节点。 +- [ ] `MovementTest.CoarsePathTest` 在后台任务调用同步 `Plan`,绘制起点、目标、路径、换向点和扩大车体检查点。 +- [ ] `TestStop` 先取消专用 `CancellationTokenSource`,再清理任务和 Painter;两个入口不得发送底盘运动命令。 +- [ ] Clumsy UI 手动运行时不阻塞界面,停止后无后台规划残留,调试开关不改变结果。 + +### Task 15(P1):性能、资源和确定性验收 + +**文件:** + +- Create: `ClumsyPilot/tests/benchmark_coarse_path.ps1` +- Modify: `Map/Planning/PlanningMapCache.cs` +- Modify: `Map/Planning/EuclideanDistanceTransform.cs` +- Modify: `CoarsePath/Search/BinaryMinHeap.cs` +- Modify: `CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `CoarsePath/Contracts/PlanningDiagnostics.cs` + +- [ ] 基准脚本输出地图规模、缓存命中、状态、耗时、扩展/生成/重开节点、陈旧堆条目、Open List 峰值和托管内存增量,超限返回非零。 +- [ ] 地图参考:20 m×20 m、0.05 m、160,000 格、100 障碍;20 次后完整构建 P95≤200 ms,完整缓存命中 P95≤5 ms。 +- [ ] 地图极限:4,000,000 格、100 障碍;3 s 内成功或明确失败;成功时内存增量≤160 MB,无溢出和部分快照。 +- [ ] 规划参考:12 m×8 m、0.05 m、矩形阻断直线、距离≥8 m;20 次 P95≤2 s,内存增量≤256 MB。 +- [ ] 规划压力:20 m×20 m、0.05 m;成功或无解均在 5 s、200,000 节点和 512 MB 增量内返回。 +- [ ] 优化只针对查询分配、重复 EDT、堆扩容和稠密点保存;不得降低碰撞保守性、跳过最终复核或放宽失败状态。 +- [ ] 运行: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj -c Release --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\benchmark_coarse_path.ps1 -Configuration Release +``` + +预期:全部指标达标,脚本退出码为 0。 + +### Task 16(P1):退役旧地图入口并完成文档 + +**文件:** + +- Modify/Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/MovementTest.Trapmaptest.cs` +- Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_grid.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_lifecycle.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_image.ps1` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` + +- [ ] 建立覆盖表:`TrapMapBounds→MapBoundsMm`、`GridMapData→EnvironmentGridMap/PlanningGridMap`、`TrapMapLayerComposer→EnvironmentMapBuilder`、`TrapMapBuilder.Get→上层采集+CoarsePathPlanningService`、旧 exporter→五个 Visualization 文件。 +- [ ] 等价脚本全部通过后再删除旧实现;不保留车辆写图、默认 300 mm 膨胀或地图构建器直接调用 `TwoLegDetect` 的兼容开关。 +- [ ] README 写明一次调用、下层门面、mm/m-rad 边界、快照变化判断、调试开关不参与指纹、P0/P1 命令和非目标。 +- [ ] 执行旧引用搜索: + +```powershell +rg -n "GridMapData|TrapMapBuilder|TrapMapLayerComposer|TrapMapImageExporter|MarkVehicleFootprint" .\ClumsyPilot +``` + +预期:仅迁移说明或历史文档可命中;运行时代码和新测试不得命中旧类型。 + +- [ ] 运行最终回归: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:Debug 构建成功,7 个功能脚本退出码均为 0;随后重跑 Task 15 Release 基准并通过。 + +## 最终验收清单 + +- [ ] `CoarsePathPlanningService.Plan(job)` 是推荐的一次调用入口。 +- [ ] `PlanningMapFactory` 和 `HybridAStarPlanner` 仅作为可独立测试的下层门面。 +- [ ] 人工、TwoLeg 和未来障碍通过同一个 `IMapObstacleSource` 进入唯一栅格化器。 +- [ ] 地图未变化时复用完整快照或占据/距离缓冲;可视化、起终点和车辆参数不参与地图变化判断。 +- [ ] 地图不包含车辆自身和安全膨胀,碰撞检查使用扩大车辆矩形。 +- [ ] 距离场是净空下界,不能因高估而跳过精确碰撞。 +- [ ] 0.50 m 原语可在任意内部采样点截断;终点候选按 Open List 顺序出队后才终止。 +- [ ] best-G、重开、陈旧条目、候选保护和堆排序均有自动化测试。 +- [ ] 输出包含稠密点、保守净空、实际曲率、换向点和完整方向分段。 +- [ ] PNG 和 MovementTest 只消费只读快照,不成为核心规划依赖。 +- [ ] 失败、取消、超时和限额均返回空路径及明确状态,不发布部分结果。 +- [ ] Debug 与 Release 验收全部通过,且未执行 Git 自检或提交。 diff --git a/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md new file mode 100644 index 0000000..0d05929 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md @@ -0,0 +1,1194 @@ +# AMR 非结构化道路轨迹规划技术路线(面向 Agent) + +## 1. 项目目标 + +开发一套面向非结构化道路四舵轮 AMR 的轨迹规划系统。 + +第一阶段采用车式运动模型,具体边界如下: + +- 支持前进、倒车和换向; +- 暂不支持蟹行、纯横移和原地旋转; +- 车体航向始终表示车头朝向; +- 规划结果必须满足完整车体无碰撞、最大曲率约束和目标位姿要求; +- 最终输出可供底盘控制器跟踪的带时间轨迹。 + +整体技术路线分为四个主要阶段: + +```mermaid +flowchart LR + A[输入与地图构建] --> B[Hybrid A* 粗路径规划] + B --> C[路径平滑与可行性处理] + C --> D[SQP 轨迹优化] + D --> E[输出可跟踪轨迹] +``` + +--- + +## 2. 总体模块划分 + +```text +Module 1:输入与地图构建 +Module 2:Hybrid A* 粗路径规划 +Module 3:路径平滑与可行性处理 +Module 4:SQP 轨迹优化 +Module 5:轨迹输出与底盘接口 +``` + +各模块必须通过明确的数据结构传递结果,不允许后级模块直接读取前级模块内部状态。 + +--- + +# 3. Module 1:输入与地图构建 + +## 3.1 模块目标 + +根据起点、终点、车辆参数和环境障碍物信息,构建一张供全局搜索使用的任务级二维栅格地图。 + +地图范围至少覆盖: + +- AMR 起点; +- 目标位置; +- 起点与目标之间的规划区域; +- 周围必要的安全边界。 + +当前阶段继续使用已有 `GridMapData` 作为地图数据来源,并通过适配器转换为 Hybrid A* 使用的规划地图。 + +--- + +## 3.2 输入数据 + +### 3.2.1 起点位姿 + +```text +StartPose: + X + Y + Heading + StartDirection + StartVehicleCurvature +``` + +字段定义: + +- `X、Y`:AMR 几何中心在世界坐标系中的位置; +- `Heading`:车头航向; +- `StartDirection`:`Forward`、`Reverse` 或 `Both`; +- `StartVehicleCurvature`:车辆当前转向曲率,未提供时默认为 0。 + +--- + +### 3.2.2 终点位姿 + +```text +GoalPose: + X + Y + Heading + GoalDirectionConstraint +``` + +目标进入方向约束: + +```text +Any +Forward +Reverse +``` + +其中: + +- `GoalHeading` 始终表示目标车头朝向; +- `GoalDirectionConstraint` 只约束最后一个连续运动段的运动方向。 + +--- + +### 3.2.3 车辆参数 + +```text +VehicleParameters: + VehicleLength + VehicleWidth + MaxVehicleCurvature + MinTurningRadius +``` + +第一阶段车辆参考点统一采用 AMR 几何中心。 + +--- + +### 3.2.4 环境障碍物 + +当前已接入: + +```text +TwoLegDetect 两腿检测结果 +``` + +后续扩展: + +```text +点云障碍物 +激光雷达障碍物 +静态障碍物 +动态障碍物 +禁行区域 +``` + +--- + +### 3.2.5 地图配置 + +```text +MapConfiguration: + GridResolution + MapMarginX + MapMarginY + ObstacleInflation + MaximumCellCount +``` + +--- + +## 3.3 当前地图构建逻辑 + +```mermaid +flowchart TD + A[读取 AMR 位姿与车身尺寸] --> B[读取目标位置与地图参数] + B --> C[计算任务级地图边界] + C --> D[创建 GridMapData] + D --> E[读取 TwoLegDetect 障碍物] + E --> F[车体系坐标转世界坐标] + F --> G[圆形障碍物栅格化] + G --> H[输出环境占据栅格] +``` + +当前地图边界由起点与目标位置共同决定: + +```text +XMin = min(StartX, GoalX) - MapMarginX +XMax = max(StartX, GoalX) + MapMarginX + +YMin = min(StartY, GoalY) - MapMarginY +YMax = max(StartY, GoalY) + MapMarginY +``` + +栅格尺寸: + +```text +Cols = ceil((XMax - XMin) / Resolution) +Rows = ceil((YMax - YMin) / Resolution) +``` + +--- + +## 3.4 当前地图适配要求 + +现有地图格式为: + +```text +单位:mm +数组:Cells[row, col] +状态:0 = 未标记占据,1 = Occupied +``` + +Hybrid A* 内部统一使用: + +```text +单位:m +角度:rad +曲率:1/m +``` + +因此需要增加: + +```text +PlanningMapAdapter +``` + +适配流程: + +```mermaid +flowchart TD + A[GridMapData] --> B[校验边界、尺寸与分辨率] + B --> C[深拷贝 Occupancy] + C --> D[毫米转换为米] + D --> E[生成障碍物距离场] + E --> F[输出 PlanningGridMap] +``` + +--- + +## 3.5 地图构建阶段的关键规则 + +### 3.5.1 规划地图不得包含 AMR 自身占据层 + +AMR 自身不能写入环境占据地图,否则 Hybrid A* 起点会直接被判定为碰撞。 + +正确关系: + +```text +环境地图: + 只包含外部障碍物 + +AMR 自身: + 通过 VehiclePose、VehicleLength、VehicleWidth 单独描述 +``` + +--- + +### 3.5.2 障碍物膨胀量必须明确 + +建议将: + +```text +SafetyMarginMm +``` + +改为: + +```text +ObstacleInflationMm +``` + +其唯一含义是: + +> 障碍物写入地图前向外膨胀的距离。 + +必须避免: + +```text +障碍物已经膨胀 ++ +车辆碰撞矩形再次增加同样余量 +``` + +--- + +### 3.5.3 区分地图创建成功与地图可用于规划 + +```text +Succeeded: + 地图数据结构是否创建成功 + +PlanningReady: + 当前地图是否满足规划输入要求 + +PlanningBlockReason: + 地图为何不能用于规划 +``` + +例如: + +```text +TwoLegDetect 正常: + Succeeded = true + PlanningReady = true + +TwoLegDetect 无结果: + Succeeded = true + PlanningReady = false +``` + +--- + +## 3.6 地图输出 + +```text +PlanningGridMap: + XMin + XMax + YMin + YMax + Resolution + Rows + Cols + Occupancy[row, col] + ObstacleDistance[row, col] + ObstacleInflation + InputSource + PlanningReady + MapVersion +``` + +当前第一版状态定义: + +```text +Occupancy = 0: + 当前地图层未标记为占据 + +Occupancy = 1: + 占据 + +地图外部: + 按占据处理 +``` + +--- + +# 4. Module 2:Hybrid A* 粗路径规划 + +## 4.1 模块目标 + +在任务级栅格地图中搜索一条: + +- 从起点到目标附近; +- 完整车体无碰撞; +- 满足最大曲率限制; +- 支持前进、倒车和换向; +- 具有正确绕障拓扑; +- 可作为后续平滑和 SQP 优化初值; + +的空间粗路径。 + +Hybrid A* 负责决定: + +```text +从哪里走 +绕障碍物哪一侧走 +什么时候前进 +什么时候倒车 +在哪里换向 +以什么车头航向接近目标 +``` + +Hybrid A* 不负责: + +```text +速度 +加速度 +角速度 +时间戳 +舵轮角度 +轮速 +``` + +--- + +## 4.2 搜索状态 + +连续状态: + +```text +State: + x + y + theta + direction + curvatureIndex +``` + +字段定义: + +- `x、y`:AMR 几何中心位置; +- `theta`:车头航向; +- `direction`:`Forward` 或 `Reverse`; +- `curvatureIndex`:当前离散车辆曲率等级。 + +Closed Set 键: + +```text +GridKey: + ix + iy + iHeading + direction + curvatureIndex +``` + +--- + +## 4.3 运动模型 + +定义: + +```text +directionSign = +1:Forward +directionSign = -1:Reverse +``` + +运动积分: + +```text +dx/ds = directionSign × cos(theta) +dy/ds = directionSign × sin(theta) +dtheta/ds = directionSign × VehicleCurvature +``` + +其中: + +```text +Heading: + 始终表示车头朝向 + +ArcLength: + 始终非负并单调增加 +``` + +--- + +## 4.4 运动原语 + +第一版离散曲率集合: + +```text +-kappaMax +-0.5 × kappaMax +0 ++0.5 × kappaMax ++kappaMax +``` + +每个曲率均支持: + +```text +Forward +Reverse +``` + +第一版建议参数: + +```text +PrimitiveLength = 0.50 m +IntegrationStep = 0.05 m +``` + +每条原语按 0.05 m 步长积分,并在每个内部积分点执行完整车体碰撞检测。 + +--- + +## 4.5 搜索约束 + +每个候选原语必须检查: + +```text +地图边界 +Occupied 栅格 +完整车辆矩形碰撞 +安全净空 +最大车辆曲率 +允许的运动方向 +曲率等级切换规则 +目标进入方向 +``` + +第一阶段暂不直接约束: + +```text +速度 +加速度 +曲率时间变化率 +舵轮转向速度 +``` + +但需要通过代价项减少极端曲率跳变。 + +--- + +## 4.6 搜索代价 + +```text +g = + 路径长度代价 + + 倒车代价 + + 换向代价 + + 曲率代价 + + 曲率变化代价 + + 障碍物距离代价 +``` + +启发函数: + +```text +h = max(h2D, hRS) +``` + +其中: + +```text +h2D: + 基于二维 Dijkstra 的绕障距离 + +hRS: + 基于 Reeds–Shepp 模型的非完整约束距离 +``` + +节点排序: + +```text +f = g + HeuristicWeight × h +``` + +--- + +## 4.7 终点附近 Reeds–Shepp 连接 + +```mermaid +flowchart TD + A[当前节点接近目标] --> B[生成 Reeds-Shepp 候选] + B --> C[按 0.05 m 采样] + C --> D[完整车体碰撞检查] + D --> E[最大曲率与目标方向检查] + E -->|成功| F[精确连接目标] + E -->|失败| G[继续 Hybrid A* 搜索] +``` + +支持模式: + +```text +Disabled +Opportunistic +RequiredNearGoal +``` + +停车、钻入和精确泊入任务优先使用: + +```text +RequiredNearGoal +``` + +--- + +## 4.8 Hybrid A* 输出 + +```text +CoarsePathPoint: + X + Y + Heading + UnwrappedHeading + ArcLength + Direction + VehicleCurvature + GeometricCurvature + BodyClearance + IsGearSwitchPoint + Source +``` + +路径按运动方向拆分: + +```text +PathSegment: + SegmentIndex + Direction + StartIndex + EndIndex + StartsAtGearSwitch + EndsAtGearSwitch +``` + +--- + +# 5. Module 3:路径平滑与可行性处理 + +## 5.1 模块目标 + +消除 Hybrid A* 粗路径中的离散折线和不连续曲率,生成更平滑的空间参考路径。 + +该阶段只优化几何形状,不进行时间参数化。 + +不得改变: + +```text +绕障侧 +前进与倒车顺序 +换向次数 +方向段拓扑 +目标进入方向 +``` + +--- + +## 5.2 处理流程 + +```mermaid +flowchart TD + A[Hybrid A* 稠密粗路径] --> B[删除重复点] + B --> C[按 Forward/Reverse 分段] + C --> D[保留关键点] + D --> E[按弧长重采样] + E --> F[几何平滑] + F --> G[完整可行性检查] + G --> H[平滑空间路径] +``` + +--- + +## 5.3 必须保留的关键点 + +```text +起点 +终点 +换向点 +曲率等级切换点 +直线与弯道切换点 +Reeds–Shepp 连接点 +局部最小净空点 +明显航向变化点 +``` + +禁止跨换向点进行单条曲线拟合。 + +--- + +## 5.4 平滑方法 + +### 5.4.1 贝塞尔曲线 + +适用于: + +```text +局部短转角 +折线拐点快速平滑 +局部连接 +``` + +特点: + +```text +实现简单 +计算速度快 +局部形状易控制 +``` + +--- + +### 5.4.2 五次多项式 + +适用于: + +```text +末端短距离连接 +具有明确起终点位置、航向和曲率的局部段 +``` + +特点: + +```text +可同时约束位置、航向和曲率 +适合末端姿态调整 +``` + +--- + +### 5.4.3 B 样条 + +适用于: + +```text +较长的普通路径段 +多点连续平滑 +降低局部折线感 +``` + +建议采用三次 B 样条作为第一版普通路径段的主平滑方法。 + +--- + +## 5.5 可行性检查 + +平滑后的路径必须重新检查: + +```text +完整车体碰撞 +地图边界 +Unknown / Occupied +最小安全净空 +最大车辆曲率 +路径位置连续性 +路径切线连续性 +换向结构 +目标进入方向 +``` + +若平滑失败: + +```text +局部重新调整 + ↓ +局部 QP 兜底 + ↓ +仍失败则回退原始可行粗路径 +``` + +--- + +## 5.6 输出 + +```text +SmoothedPathPoint: + X + Y + Heading + ArcLength + Direction + VehicleCurvature + GeometricCurvature + BodyClearance + Source +``` + +该输出仍然不包含: + +```text +速度 +加速度 +时间戳 +``` + +--- + +# 6. Module 4:SQP 轨迹优化 + +## 6.1 模块目标 + +以平滑空间路径作为参考线,通过 Frenet 坐标系和 SQP 优化,联合完成: + +- 横向几何调整; +- 曲率优化; +- 纵向速度规划; +- 加速度约束; +- 换向停车约束; +- 终点精确收敛; +- 时间参数化。 + +最终生成可供车辆控制器跟踪的带时间轨迹。 + +--- + +## 6.2 Frenet 坐标转换 + +将平滑路径作为参考线,建立 Frenet 坐标系: + +```text +s: + 沿参考线的累计弧长 + +l: + 相对于参考线的横向偏移 +``` + +转换后,将原二维路径问题分解为: + +```text +横向 l-s 问题 +纵向 s-t 问题 +``` + +--- + +## 6.3 横向 l-s 优化 + +主要优化变量: + +```text +横向偏移 l +横向一阶导数 dl/ds +横向二阶导数 d²l/ds² +曲率 +曲率变化 +``` + +优化目标: + +```text +尽量贴近参考线 +避开障碍物 +保持曲率平滑 +满足最大曲率 +满足安全走廊约束 +精确到达目标横向位置 +``` + +--- + +## 6.4 纵向 s-t 优化 + +主要优化变量: + +```text +s(t) +速度 v(t) +加速度 a(t) +加加速度 jerk(t) +``` + +优化目标: + +```text +满足速度上限 +满足加速度约束 +满足减速度约束 +提高运动平顺性 +减少总时间 +在换向点停车 +在终点停车 +``` + +--- + +## 6.5 SQP 联合优化 + +SQP 负责迭代求解: + +```text +路径位置 +航向 +曲率 +速度 +加速度 +角速度 +时间戳 +``` + +约束至少包括: + +```text +障碍物安全约束 +车辆曲率约束 +速度约束 +加速度约束 +角速度约束 +换向点速度为 0 +终点位置约束 +终点航向约束 +终点速度约束 +``` + +SQP 不负责: + +```text +重新决定绕障侧 +凭空新增换向 +改变 Hybrid A* 已确定的拓扑结构 +``` + +--- + +## 6.6 SQP 输出 + +```text +TrajectoryPoint: + X + Y + Heading + Time + ArcLength + Direction + Velocity + Acceleration + AngularVelocity + VehicleCurvature + GeometricCurvature +``` + +该轨迹可以直接提供给后续底盘控制与轨迹跟踪模块。 + +--- + +# 7. Module 5:轨迹输出与底盘接口 + +## 7.1 输出内容 + +```text +TrajectoryResult: + PlanningStatus + GoalReachType + PathProcessingResult + TrajectoryPoints + DirectionSegments + Diagnostics +``` + +--- + +## 7.2 底盘控制前的处理 + +轨迹规划器不直接输出四轮舵角和轮速。 + +底盘控制层负责: + +```text +轨迹点 + ↓ +车辆状态误差计算 + ↓ +轨迹跟踪控制 + ↓ +底盘运动学逆解 + ↓ +四轮舵角与轮速 +``` + +--- + +# 8. Agent 开发顺序 + +## 阶段 M0:数据结构与公共工具 + +实现: + +```text +Pose2D +VehicleParameters +GoalPose +GridMapData Adapter +角度归一化 +坐标变换 +单位转换 +``` + +完成标准: + +```text +单元测试通过 +毫米与米转换无误 +航向角定义统一 +``` + +--- + +## 阶段 M1:地图适配与碰撞检测 + +实现: + +```text +PlanningMapAdapter +PlanningGridMap +距离场 +旋转矩形碰撞检测 +地图边界检测 +``` + +完成标准: + +```text +AMR 自身不会出现在环境占据图中 +不同航向下完整车体碰撞检测正确 +``` + +--- + +## 阶段 M2:最小 Hybrid A* + +实现: + +```text +只前进 +无障碍 +恒曲率运动原语 +基础启发函数 +``` + +完成标准: + +```text +稳定从起点搜索到目标 +``` + +--- + +## 阶段 M3:障碍物、倒车和换向 + +实现: + +```text +Dijkstra 启发 +Forward / Reverse +换向代价 +方向分段 +``` + +完成标准: + +```text +单障碍绕行成功 +需要倒车的场景成功 +换向点正确输出 +``` + +--- + +## 阶段 M4:目标终止与 Reeds–Shepp + +实现: + +```text +目标位置容差 +目标航向容差 +目标进入方向 +Reeds–Shepp 三种模式 +``` + +完成标准: + +```text +普通目标和狭窄目标场景均能正确终止 +``` + +--- + +## 阶段 M5:路径后处理 + +实现: + +```text +父节点回溯 +内部积分点恢复 +关键点保留 +弧长重采样 +``` + +完成标准: + +```text +路径无断点 +换向结构明确 +方向段正确 +``` + +--- + +## 阶段 M6:平滑与可行性检查 + +实现: + +```text +B 样条 +贝塞尔曲线 +五次多项式 +完整碰撞复核 +失败回退 +``` + +完成标准: + +```text +路径曲率明显改善 +不改变原有拓扑 +平滑失败可回退 +``` + +--- + +## 阶段 M7:SQP 轨迹优化 + +实现: + +```text +Frenet 坐标转换 +l-s 优化 +s-t 优化 +速度和加速度约束 +换向停车约束 +终点精确约束 +``` + +完成标准: + +```text +输出带时间的可跟踪轨迹 +终点位置与航向满足要求 +换向点速度为 0 +``` + +--- + +## 阶段 M8:性能优化 + +在前述流程完整跑通后再开展: + +```text +性能剖析 +Open List 优化 +Closed Set 优化 +节点内存优化 +地图裁剪 +预计算运动原语 +预计算碰撞模板 +启发图复用 +路径热启动 +增量规划 +``` + +--- + +# 9. 核心技术边界 + +## Hybrid A* 负责 + +```text +拓扑 +绕障侧 +前进与倒车结构 +换向位置 +基本运动学可行性 +粗目标位姿接近 +``` + +## 路径平滑负责 + +```text +消除折线 +改善局部曲率 +保持拓扑不变 +输出连续空间参考路径 +``` + +## SQP 负责 + +```text +终点精确收敛 +速度规划 +加速度规划 +角速度规划 +时间参数化 +动态可跟踪性 +``` + +## 底盘控制负责 + +```text +四轮舵角 +四轮轮速 +轨迹跟踪 +执行器控制 +``` + +--- + +# 10. 最终技术路线总结 + +```text +起点位姿 + 目标位姿 + 车辆参数 + 障碍物数据 + ↓ +构建任务级环境占据地图 + ↓ +通过 PlanningMapAdapter 转换为规划地图 + ↓ +Hybrid A* 搜索满足运动学约束的粗路径 + ↓ +Reeds–Shepp 处理终点位姿连接 + ↓ +回溯、重采样和路径方向分段 + ↓ +B 样条 / 贝塞尔 / 五次多项式进行几何平滑 + ↓ +重新执行完整车体碰撞和曲率检查 + ↓ +建立 Frenet 参考坐标系 + ↓ +横向 l-s 优化路径形状和曲率 + ↓ +纵向 s-t 优化速度、加速度和停车决策 + ↓ +SQP 联合求解可跟踪轨迹 + ↓ +输出位置、航向、速度、加速度、角速度和时间戳 + ↓ +底盘控制器进行四轮舵角和轮速逆解 +``` + +该技术路线的核心原则是: + +> Hybrid A* 解决“怎么走”,路径平滑解决“走得是否顺”,SQP 解决“以什么速度和时间走”,底盘控制解决“四个舵轮和车轮如何执行”。 diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs new file mode 100644 index 0000000..f4097e3 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs @@ -0,0 +1,92 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 建图阶段使用的可变环境占据栅格。 +/// +/// 单位:边界、世界查询和栅格边长均为 mm。 +/// 注意:只有 可以写入占据状态;规划阶段应改用不可变的 。 +/// +public sealed class EnvironmentGridMap +{ + private readonly byte[] _cells; + private int _occupiedCount; + + /// + /// 创建空的环境占据栅格。 + /// + /// 参数:bounds 为左闭右开的世界边界,单位 mm;resolutionMm 为格边长,单位 mm。 + /// 返回:无;边界为空或分辨率不合法时抛出异常。 + /// + public EnvironmentGridMap(MapBoundsMm bounds, float resolutionMm) + { + if (bounds == null) throw new ArgumentNullException(nameof(bounds)); + bounds.GetDimensions(resolutionMm, out int rows, out int cols); + Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; + _cells = new byte[checked(rows * cols)]; + } + + /// 地图世界边界,单位 mm,采用左闭右开规则。 + public MapBoundsMm Bounds { get; } + /// 单个栅格边长,单位 mm。 + public float ResolutionMm { get; } + /// 栅格行数,Y 方向从下限向上递增。 + public int Rows { get; } + /// 栅格列数,X 方向从下限向右递增。 + public int Cols { get; } + /// 当前已被标记为障碍的格数。 + public int OccupiedCount { get { return _occupiedCount; } } + + /// 判断行列索引是否有效。参数 row、col 分别为从零开始的行和列;有效时返回 true。 + public bool IsInBounds(int row, int col) { return row >= 0 && row < Rows && col >= 0 && col < Cols; } + /// 判断世界坐标是否位于地图内。参数 xMm、yMm 单位为 mm;上边界与右边界返回 false。 + public bool IsWorldInBounds(float xMm, float yMm) { return Bounds.Contains(xMm, yMm); } + + /// + /// 将世界坐标转换为栅格索引。 + /// + /// 参数:xMm、yMm 为世界坐标,单位 mm;row、col 为输出索引。 + /// 返回:坐标在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 + /// + public bool TryWorldToGrid(float xMm, float yMm, out int row, out int col) + { + row = -1; col = -1; + if (!IsWorldInBounds(xMm, yMm)) return false; + col = (int)Math.Floor(((double)xMm - Bounds.XMin) / ResolutionMm); + row = (int)Math.Floor(((double)yMm - Bounds.YMin) / ResolutionMm); + return IsInBounds(row, col); + } + + /// 查询栅格是否占据。越界索引按障碍处理,返回 true。 + public bool IsOccupied(int row, int col) { return !IsInBounds(row, col) || _cells[row * Cols + col] != 0; } + /// 按世界坐标查询占据状态。参数 xMm、yMm 单位为 mm;坐标越界时保守地返回 true。 + public bool IsOccupiedWorld(float xMm, float yMm) + { + return !TryWorldToGrid(xMm, yMm, out int row, out int col) || IsOccupied(row, col); + } + + /// + /// 获取一个栅格的世界坐标范围。 + /// + /// 参数:row、col 为有效索引;xMin、xMax、yMin、yMax 为输出边界,单位 mm。 + /// 返回:无;索引越界时抛出 。 + /// + public void GetCellBounds(int row, int col, out float xMin, out float xMax, out float yMin, out float yMax) + { + if (!IsInBounds(row, col)) throw new ArgumentOutOfRangeException(); + xMin = Bounds.XMin + col * ResolutionMm; + yMin = Bounds.YMin + row * ResolutionMm; + xMax = Math.Min(Bounds.XMax, xMin + ResolutionMm); + yMax = Math.Min(Bounds.YMax, yMin + ResolutionMm); + } + + internal void MarkOccupied(int row, int col) + { + if (!IsInBounds(row, col)) return; + int index = row * Cols + col; + if (_cells[index] == 0) { _cells[index] = 1; _occupiedCount++; } + } + internal byte[] CopyCells() { return (byte[])_cells.Clone(); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs new file mode 100644 index 0000000..d084079 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 环境占据图构建结果。 +/// 返回:成功时提供可供适配的 EnvironmentGridMap;失败时提供失败原因和已处理来源状态。 +/// +public sealed class EnvironmentMapBuildResult +{ + private EnvironmentMapBuildResult(bool succeeded, EnvironmentGridMap map, IReadOnlyList sourceResults, string failureReason, PlanningOperationStopReason stopReason) + { + Succeeded = succeeded; Map = map; SourceResults = sourceResults ?? Array.Empty(); FailureReason = failureReason ?? string.Empty; StopReason = stopReason; + } + /// 构建是否成功。true 时 Map 非空;false 时读取 FailureReason。 + public bool Succeeded { get; } + /// 成功生成的构建期环境栅格;失败时为 null。 + public EnvironmentGridMap Map { get; } + /// 已尝试来源的投影结果,用于记录已应用、空或失败状态。 + public IReadOnlyList SourceResults { get; } + /// 失败原因。成功时为空字符串。 + public string FailureReason { get; } + /// 内部预算停止原因;普通构建成功或失败时为 None。 + internal PlanningOperationStopReason StopReason { get; } + /// 创建成功结果。参数 map 为已完成栅格,sourceResults 为来源投影记录。 + public static EnvironmentMapBuildResult Success(EnvironmentGridMap map, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(true, map, sourceResults, null, PlanningOperationStopReason.None); } + /// 创建失败结果。参数 reason 为诊断文本,sourceResults 可包含失败前已处理的来源。 + public static EnvironmentMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(false, null, sourceResults, reason, PlanningOperationStopReason.None); } + /// 创建已取消或超时结果;不发布构建期可写地图。 + internal static EnvironmentMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) + { + if (stopReason == PlanningOperationStopReason.None) throw new ArgumentOutOfRangeException(nameof(stopReason)); + return new EnvironmentMapBuildResult(false, null, sourceResults, + stopReason == PlanningOperationStopReason.Cancelled ? "地图构建已取消。" : "地图构建已超时。", stopReason); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs new file mode 100644 index 0000000..31e4c0d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 从排序后的纯障碍物快照事务性构建环境占据图。 +/// +/// 注意:必需来源返回不可用或无效状态时,构建整体失败;可选来源仅记录其状态并继续构建。 +/// +public sealed class EnvironmentMapBuilder +{ + /// + /// 投影所有障碍物来源并栅格化为环境地图。 + /// + /// 参数:request 包含 mm 世界边界、分辨率和来源列表;每个来源 ID 必须唯一且版本非负。 + /// 返回:成功时包含 和全部来源状态;必需来源失败时返回失败结果而不产生可用地图。 + /// + public EnvironmentMapBuildResult Build(MapBuildRequest request) + { + return Build(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); + } + + /// 使用共享预算投影来源并栅格化;停止时不发布可写环境地图。 + internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget) + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, null); + if (request == null || request.Bounds == null) return EnvironmentMapBuildResult.Failure("Map request and bounds are required.", null); + if (request.ObstacleSources == null) return EnvironmentMapBuildResult.Failure("Obstacle source collection is required.", null); + var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); + var results = new List(); + string previousId = null; + for (int i = 0; i < sources.Length; i++) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); + IMapObstacleSource source = sources[i]; + if (source == null || string.IsNullOrWhiteSpace(source.SourceId) || source.SourceVersion < 0) + return EnvironmentMapBuildResult.Failure("Each source needs a non-empty id and non-negative version.", results); + if (string.Equals(previousId, source.SourceId, StringComparison.Ordinal)) + return EnvironmentMapBuildResult.Failure("Obstacle source ids must be unique.", results); + previousId = source.SourceId; + ObstacleProjectionResult result; + try { result = source.ProjectToWorld() ?? ObstacleProjectionResult.Invalid("Source returned no projection result."); } + catch (Exception exception) { result = ObstacleProjectionResult.Invalid(exception.Message); } + results.Add(result); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); + if (source.IsRequired && (result.Status == ObstacleSourceStatus.Invalid || result.Status == ObstacleSourceStatus.Unavailable)) + return EnvironmentMapBuildResult.Failure("A required obstacle source failed: " + source.SourceId, results); + } + var map = new EnvironmentGridMap(request.Bounds, request.ResolutionMm); + for (int i = 0; i < results.Count; i++) + if (results[i].Status == ObstacleSourceStatus.Applied) + for (int j = 0; j < results[i].Obstacles.Count; j++) + { + if (!MapObstacleRasterizer.TryRasterize(map, results[i].Obstacles[j], budget, out stopReason)) + return EnvironmentMapBuildResult.Stopped(stopReason, results); + } + return EnvironmentMapBuildResult.Success(map, results); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs new file mode 100644 index 0000000..96c707b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs @@ -0,0 +1,86 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 有限的世界地图边界。 +/// 单位:mm;范围采用左闭右开 [XMin, XMax) × [YMin, YMax)。 +/// +public sealed class MapBoundsMm : IEquatable +{ + /// 单张地图允许的最大栅格数,超过该值会拒绝创建地图。 + public const int MaximumCellCount = 4000000; + + /// + /// 创建地图世界边界。 + /// + /// 参数: + /// - xMin、xMax:世界 X 轴下限和上限,单位 mm,且 xMax 必须大于 xMin。 + /// - yMin、yMax:世界 Y 轴下限和上限,单位 mm,且 yMax 必须大于 yMin。 + /// + /// 注意:边界采用左闭右开规则,上限坐标不属于地图。 + /// + public MapBoundsMm(float xMin, float xMax, float yMin, float yMax) + { + if (!NumericGuard.IsFinite(xMin) || !NumericGuard.IsFinite(xMax) || + !NumericGuard.IsFinite(yMin) || !NumericGuard.IsFinite(yMax) || + xMax <= xMin || yMax <= yMin) + throw new ArgumentOutOfRangeException(nameof(xMax), "Map bounds must be finite and non-degenerate."); + XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; + } + + /// 世界 X 轴下限,单位 mm,包含在地图内。 + public float XMin { get; } + /// 世界 X 轴上限,单位 mm,不包含在地图内。 + public float XMax { get; } + /// 世界 Y 轴下限,单位 mm,包含在地图内。 + public float YMin { get; } + /// 世界 Y 轴上限,单位 mm,不包含在地图内。 + public float YMax { get; } + + /// + /// 判断世界坐标是否属于地图边界。 + /// + /// 参数:xMm、yMm 为世界坐标,单位 mm。 + /// 返回:坐标位于 [XMin, XMax) × [YMin, YMax) 时为 true,否则为 false。 + /// + public bool Contains(float xMm, float yMm) + { + return xMm >= XMin && xMm < XMax && yMm >= YMin && yMm < YMax; + } + + /// + /// 根据栅格分辨率计算行列数。 + /// + /// 参数:resolutionMm 为每个方格的边长,单位 mm,取值必须在 [20, 200];rows、cols 为输出行数和列数。 + /// 返回:无;当分辨率无效或总格数超过 时抛出异常。 + /// + public void GetDimensions(float resolutionMm, out int rows, out int cols) + { + if (!NumericGuard.IsInRange(resolutionMm, 20f, 200f)) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "ResolutionMm must be within [20, 200]."); + double columnCount = Math.Ceiling(((double)XMax - XMin) / resolutionMm); + double rowCount = Math.Ceiling(((double)YMax - YMin) / resolutionMm); + if (columnCount > int.MaxValue || rowCount > int.MaxValue || columnCount <= 0d || rowCount <= 0d) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map dimensions are invalid."); + cols = (int)columnCount; rows = (int)rowCount; + long cellCount = checked((long)rows * cols); + if (cellCount > MaximumCellCount) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map cell count exceeds 4,000,000."); + } + + /// 比较两个边界的四个 mm 坐标是否完全相同。 + public bool Equals(MapBoundsMm other) + { + return other != null && XMin.Equals(other.XMin) && XMax.Equals(other.XMax) && + YMin.Equals(other.YMin) && YMax.Equals(other.YMax); + } + /// 比较当前边界与指定对象是否表示相同的世界范围。 + public override bool Equals(object obj) { return Equals(obj as MapBoundsMm); } + /// 返回由四个边界坐标组成的哈希值,用于缓存键比较。 + public override int GetHashCode() + { + unchecked { int hash = XMin.GetHashCode(); hash = hash * 31 + XMax.GetHashCode(); hash = hash * 31 + YMin.GetHashCode(); return hash * 31 + YMax.GetHashCode(); } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs new file mode 100644 index 0000000..5c8290e --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 环境占据图构建器的输入数据。 +/// 注意:通常由 PlanningMapFactory 从公开请求转换得到,调用者无需直接使用。 +/// +public sealed class MapBuildRequest +{ + /// 环境图世界边界。单位:mm;不能为空。 + public MapBoundsMm Bounds { get; set; } + /// 环境栅格边长。单位:mm;必须满足 MapBoundsMm 的分辨率限制。 + public float ResolutionMm { get; set; } + /// 待投影的障碍物来源列表;每个来源 ID 必须唯一。 + public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs new file mode 100644 index 0000000..e9ce924 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs @@ -0,0 +1,31 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 边与世界 X/Y 轴平行的矩形障碍物,坐标单位为 mm。 +public sealed class AxisAlignedRectangleObstacle : IMapObstacle +{ + /// + /// 创建轴对齐矩形障碍物。 + /// + /// 参数:xMin、xMax、yMin、yMax 分别为矩形世界坐标边界,单位 mm。 + /// 注意:构造不校验边界顺序,请通过 判断后再使用。 + /// + public AxisAlignedRectangleObstacle(float xMin, float xMax, float yMin, float yMax) + { + XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; + } + /// 矩形世界 X 下边界,单位 mm。 + public float XMin { get; } + /// 矩形世界 X 上边界,单位 mm。 + public float XMax { get; } + /// 矩形世界 Y 下边界,单位 mm。 + public float YMin { get; } + /// 矩形世界 Y 上边界,单位 mm。 + public float YMax { get; } + /// 四个边界均为有限数且 XMax≥XMin、YMax≥YMin 时为 true;否则为 false。 + public bool IsValid + { + get { return NumericGuard.IsFinite(XMin) && NumericGuard.IsFinite(XMax) && NumericGuard.IsFinite(YMin) && NumericGuard.IsFinite(YMax) && XMax >= XMin && YMax >= YMin; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs new file mode 100644 index 0000000..55eee5f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs @@ -0,0 +1,29 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 以世界 mm 坐标表示的圆形障碍物。 +public sealed class CircleObstacle : IMapObstacle +{ + /// + /// 创建圆形障碍物。 + /// + /// 参数:centerX、centerY 为圆心世界坐标,单位 mm;radiusMm 为半径,单位 mm。 + /// 注意:构造不抛出几何校验异常,请通过 判断后再使用。 + /// + public CircleObstacle(float centerX, float centerY, float radiusMm) + { + CenterX = centerX; CenterY = centerY; RadiusMm = radiusMm; + } + /// 圆心世界 X 坐标,单位 mm。 + public float CenterX { get; } + /// 圆心世界 Y 坐标,单位 mm。 + public float CenterY { get; } + /// 圆的半径,单位 mm。 + public float RadiusMm { get; } + /// 圆心和半径均为有限数且半径不小于零时为 true;否则为 false。 + public bool IsValid + { + get { return NumericGuard.IsFinite(CenterX) && NumericGuard.IsFinite(CenterY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs new file mode 100644 index 0000000..2377271 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs @@ -0,0 +1,12 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 世界坐标中的不可变障碍物几何。 +/// +/// 单位:所有几何坐标与尺寸均为 mm。实现类型必须能由 栅格化。 +/// +public interface IMapObstacle +{ + /// 几何数据是否有效。true 表示数值有限且尺寸满足该几何类型的约束;false 表示不得投影到地图。 + bool IsValid { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs new file mode 100644 index 0000000..44d35de --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 将障碍物几何保守投影为栅格占据状态的唯一写入入口。 +/// +/// 注意:调用方不能直接改写 ;相交或贴边的栅格均按占据处理。 +/// +public static class MapObstacleRasterizer +{ + /// + /// 将一个有效障碍物栅格化到环境地图。 + /// + /// 参数:map 为待写入的环境栅格;obstacle 为世界 mm 坐标的圆形或轴对齐矩形障碍物。 + /// 返回:无。地图或障碍物为空、障碍物无效、几何类型不受支持时抛出异常。 + /// 注意:该方法只增加占据格,不会清除已有障碍。 + /// + public static void Rasterize(EnvironmentGridMap map, IMapObstacle obstacle) + { + if (!TryRasterize(map, obstacle, PlanningOperationBudget.Unlimited(CancellationToken.None), out _)) + throw new InvalidOperationException("Unbounded rasterization unexpectedly stopped."); + } + + /// 使用共享预算将障碍物写入环境栅格;停止时返回 false 且不发布环境地图。 + internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, PlanningOperationBudget budget, + out PlanningOperationStopReason stopReason) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + if (obstacle == null || !obstacle.IsValid) throw new ArgumentException("Obstacle must be valid.", nameof(obstacle)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + int workItemCount = 0; + var circle = obstacle as CircleObstacle; + if (circle != null) return TryRasterizeCircle(map, circle, budget, ref workItemCount, out stopReason); + var rectangle = obstacle as AxisAlignedRectangleObstacle; + if (rectangle != null) return TryRasterizeRectangle(map, rectangle, budget, ref workItemCount, out stopReason); + throw new NotSupportedException("Unsupported map obstacle geometry."); + } + + private static bool TryRasterizeCircle(EnvironmentGridMap map, CircleObstacle circle, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + GetCandidateRange(map, circle.CenterX - circle.RadiusMm, circle.CenterX + circle.RadiusMm, + circle.CenterY - circle.RadiusMm, circle.CenterY + circle.RadiusMm, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + double radiusSquared = (double)circle.RadiusMm * circle.RadiusMm; + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); + double nearestX = Math.Max(xMin, Math.Min(circle.CenterX, xMax)); + double nearestY = Math.Max(yMin, Math.Min(circle.CenterY, yMax)); + double dx = circle.CenterX - nearestX; + double dy = circle.CenterY - nearestY; + if (dx * dx + dy * dy <= radiusSquared) map.MarkOccupied(row, col); + } + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static bool TryRasterizeRectangle(EnvironmentGridMap map, AxisAlignedRectangleObstacle rectangle, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + GetCandidateRange(map, rectangle.XMin, rectangle.XMax, rectangle.YMin, rectangle.YMax, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); + if (rectangle.XMax >= xMin && rectangle.XMin <= xMax && rectangle.YMax >= yMin && rectangle.YMin <= yMax) + map.MarkOccupied(row, col); + } + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static void GetCandidateRange(EnvironmentGridMap map, float xMin, float xMax, float yMin, float yMax, + out int firstRow, out int lastRow, out int firstCol, out int lastCol) + { + if (xMax < map.Bounds.XMin || xMin > map.Bounds.XMax || yMax < map.Bounds.YMin || yMin > map.Bounds.YMax) + { firstRow = 1; lastRow = 0; firstCol = 1; lastCol = 0; return; } + // Geometry is closed for conservative rasterisation. Include the cell on + // the lower side when a boundary lies exactly on a grid line. + firstCol = Clamp((int)Math.Floor(((double)xMin - map.Bounds.XMin) / map.ResolutionMm) - 1, 0, map.Cols - 1); + lastCol = Clamp((int)Math.Floor(((double)xMax - map.Bounds.XMin) / map.ResolutionMm), 0, map.Cols - 1); + firstRow = Clamp((int)Math.Floor(((double)yMin - map.Bounds.YMin) / map.ResolutionMm) - 1, 0, map.Rows - 1); + lastRow = Clamp((int)Math.Floor(((double)yMax - map.Bounds.YMin) / map.ResolutionMm), 0, map.Rows - 1); + } + private static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs new file mode 100644 index 0000000..2bdba59 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 针对行主序二值栅格计算精确欧氏距离平方的内部算法。 +internal static class EuclideanDistanceTransform +{ + /// + /// 计算每个栅格到最近障碍栅格的距离平方。 + /// + /// 参数:occupied 为行主序占据数组,非零表示障碍;rows、cols 为数组尺寸。 + /// 返回:行主序距离平方数组,单位为栅格边长的平方;不含任何 mm 或 m 换算。 + /// + public static double[] ComputeSquaredDistances(byte[] occupied, int rows, int cols) + { + if (!TryComputeSquaredDistances(occupied, rows, cols, PlanningOperationBudget.Unlimited(CancellationToken.None), + out double[] squared, out _)) + throw new InvalidOperationException("Unbounded Euclidean distance transform unexpectedly stopped."); + return squared; + } + + /// 使用共享预算计算距离平方;停止时不返回部分数组。 + internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, PlanningOperationBudget budget, + out double[] squaredDistances, out PlanningOperationStopReason stopReason) + { + if (occupied == null) throw new ArgumentNullException(nameof(occupied)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + squaredDistances = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + double noObstacleDistanceSquared = (double)rows * rows + (double)cols * cols + 1d; + var intermediate = new double[occupied.Length]; + var result = new double[occupied.Length]; + var input = new double[Math.Max(rows, cols)]; + var output = new double[Math.Max(rows, cols)]; + int workItemCount = 0; + for (int row = 0; row < rows; row++) + { + int offset = row * cols; + for (int col = 0; col < cols; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + input[col] = occupied[offset + col] == 0 ? noObstacleDistanceSquared : 0d; + } + if (!TryTransform1D(input, cols, output, budget, ref workItemCount, out stopReason)) return false; + for (int col = 0; col < cols; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + intermediate[offset + col] = output[col]; + } + } + for (int col = 0; col < cols; col++) + { + for (int row = 0; row < rows; row++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + input[row] = intermediate[row * cols + col]; + } + if (!TryTransform1D(input, rows, output, budget, ref workItemCount, out stopReason)) return false; + for (int row = 0; row < rows; row++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + result[row * cols + col] = output[row]; + } + } + squaredDistances = result; + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static bool TryTransform1D(double[] f, int length, double[] result, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + var locations = new int[length]; + var boundaries = new double[length + 1]; + int k = 0; + locations[0] = 0; + boundaries[0] = double.NegativeInfinity; + boundaries[1] = double.PositiveInfinity; + for (int q = 1; q < length; q++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + double intersection; + do + { + int p = locations[k]; + intersection = ((f[q] + (double)q * q) - (f[p] + (double)p * p)) / (2d * (q - p)); + if (intersection <= boundaries[k]) + { + k--; + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + } + } while (k >= 0 && intersection <= boundaries[k]); + if (k < 0) + { + k = 0; locations[0] = q; boundaries[0] = double.NegativeInfinity; boundaries[1] = double.PositiveInfinity; + } + else + { + k++; locations[k] = q; boundaries[k] = intersection; boundaries[k + 1] = double.PositiveInfinity; + } + } + k = 0; + for (int q = 0; q < length; q++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + while (boundaries[k + 1] < q) k++; + double delta = q - locations[k]; + result[q] = delta * delta + f[locations[k]]; + } + stopReason = PlanningOperationStopReason.None; + return true; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs new file mode 100644 index 0000000..385551d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 以 m 表示的障碍物净距离保守下界。 +internal sealed class ObstacleDistanceField +{ + private readonly double[] _conservativeDistances; + private ObstacleDistanceField(double[] conservativeDistances) { _conservativeDistances = conservativeDistances; } + /// + /// 从占据栅格创建距离场。 + /// + /// 参数:occupied 为行主序占据数组;rows、cols 为其尺寸;resolutionMeters 为格边长,单位 m。 + /// 返回:每个格到最近障碍物的保守净距离下界,单位 m;全空地图中的每项为正无穷。 + /// + public static ObstacleDistanceField Create(byte[] occupied, int rows, int cols, double resolutionMeters) + { + if (!TryCreate(occupied, rows, cols, resolutionMeters, PlanningOperationBudget.Unlimited(CancellationToken.None), + out ObstacleDistanceField field, out _)) + throw new InvalidOperationException("Unbounded distance-field creation unexpectedly stopped."); + return field; + } + + /// 使用共享预算创建距离场;停止时不返回部分距离数据。 + internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, + PlanningOperationBudget budget, out ObstacleDistanceField field, out PlanningOperationStopReason stopReason) + { + if (occupied == null) throw new ArgumentNullException(nameof(occupied)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + field = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + bool hasObstacle = false; + int workItemCount = 0; + for (int i = 0; i < occupied.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + if (occupied[i] != 0) { hasObstacle = true; break; } + } + var distances = new double[occupied.Length]; + if (!hasObstacle) + { + for (int i = 0; i < distances.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + distances[i] = double.PositiveInfinity; + } + field = new ObstacleDistanceField(distances); + stopReason = PlanningOperationStopReason.None; + return true; + } + if (!EuclideanDistanceTransform.TryComputeSquaredDistances(occupied, rows, cols, budget, out double[] squared, out stopReason)) + return false; + double conservativeOffset = Math.Sqrt(2d) * resolutionMeters; + for (int i = 0; i < distances.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + distances[i] = Math.Max(0d, Math.Sqrt(squared[i]) * resolutionMeters - conservativeOffset); + } + field = new ObstacleDistanceField(distances); + stopReason = PlanningOperationStopReason.None; + return true; + } + internal double[] CopyDistances() { return (double[])_conservativeDistances.Clone(); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs new file mode 100644 index 0000000..0c014ca --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs @@ -0,0 +1,88 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 供粗路径规划使用的不可变地图快照。 +/// +/// 单位:世界查询方法使用 m; 保留原始 mm 数据。 +/// 注意:世界坐标越界一律按占据处理,净距离为零。 +/// +public sealed class PlanningGridMap +{ + private readonly byte[] _occupied; + private readonly double[] _conservativeDistances; + + internal PlanningGridMap(MapBoundsMm bounds, float resolutionMm, int rows, int cols, byte[] occupied, double[] conservativeDistances, + long snapshotId, bool planningReady, string planningBlockReason, string inputFingerprint, string occupancyHash) + { + Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; + _occupied = occupied; _conservativeDistances = conservativeDistances; + SnapshotId = snapshotId; PlanningReady = planningReady; PlanningBlockReason = planningBlockReason ?? string.Empty; + InputFingerprint = inputFingerprint ?? string.Empty; OccupancyHash = occupancyHash ?? string.Empty; + } + /// 源环境图的世界边界,单位 mm,采用左闭右开规则。 + public MapBoundsMm Bounds { get; } + /// 源环境图的栅格边长,单位 mm。 + public float ResolutionMm { get; } + /// 规划世界查询对应的栅格边长,单位 m。 + public double ResolutionMeters { get { return ResolutionMm / 1000d; } } + /// 栅格行数。 + public int Rows { get; } + /// 栅格列数。 + public int Cols { get; } + /// 工厂为本次返回快照分配的单调编号,用于区分不同构建结果。 + public long SnapshotId { get; } + /// 地图是否允许进入粗路径规划。true 时可直接查询;false 时应先处理 + public bool PlanningReady { get; } + /// 禁止规划的原因。 为 true 时为空字符串。 + public string PlanningBlockReason { get; } + /// 完整建图输入的稳定指纹,用于识别精确输入缓存命中。 + public string InputFingerprint { get; } + /// 占据栅格内容哈希,用于识别可复用的占据与距离数组。 + public string OccupancyHash { get; } + + /// 查询世界位置是否占据。参数 xMeters、yMeters 单位为 m;位置越界时保守地返回 true。 + public bool IsOccupiedWorld(double xMeters, double yMeters) + { + return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) || _occupied[row * Cols + col] != 0; + } + /// + /// 查询到最近障碍物的保守净距离下界。 + /// + /// 参数:xMeters、yMeters 为世界坐标,单位 m。 + /// 返回:单位 m 的非负距离下界;地图内无障碍物时为正无穷,越界时为零。 + /// + public double GetConservativeObstacleDistanceMeters(double xMeters, double yMeters) + { + return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) ? 0d : _conservativeDistances[row * Cols + col]; + } + /// + /// 将规划世界坐标转换为栅格索引。 + /// + /// 参数:xMeters、yMeters 为世界坐标,单位 m;row、col 为输出索引。 + /// 返回:位置在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 + /// + public bool TryWorldToGrid(double xMeters, double yMeters, out int row, out int col) + { + row = -1; col = -1; + double xMm = xMeters * 1000d, yMm = yMeters * 1000d; + if (xMm < Bounds.XMin || xMm >= Bounds.XMax || yMm < Bounds.YMin || yMm >= Bounds.YMax) return false; + col = (int)Math.Floor((xMm - Bounds.XMin) / ResolutionMm); + row = (int)Math.Floor((yMm - Bounds.YMin) / ResolutionMm); + return row >= 0 && row < Rows && col >= 0 && col < Cols; + } + /// 按行列索引查询占据状态。参数从零开始;任一索引越界时返回 true。 + public bool IsOccupied(int row, int col) { return row < 0 || row >= Rows || col < 0 || col >= Cols || _occupied[row * Cols + col] != 0; } + internal byte[] CopyOccupied() { return (byte[])_occupied.Clone(); } + internal bool OccupancyEquals(PlanningGridMap other) + { + if (other == null || Rows != other.Rows || Cols != other.Cols || ResolutionMm != other.ResolutionMm || !Bounds.Equals(other.Bounds) || _occupied.Length != other._occupied.Length) return false; + for (int i = 0; i < _occupied.Length; i++) if (_occupied[i] != other._occupied[i]) return false; + return true; + } + internal PlanningGridMap WithMetadata(long snapshotId, bool planningReady, string blockReason, string inputFingerprint, string occupancyHash) + { + return new PlanningGridMap(Bounds, ResolutionMm, Rows, Cols, _occupied, _conservativeDistances, snapshotId, planningReady, blockReason, inputFingerprint, occupancyHash); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs new file mode 100644 index 0000000..3c292d9 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将建图阶段的 mm 环境栅格适配为规划阶段的不可变 m 查询快照。 +public static class PlanningMapAdapter +{ + /// + /// 创建规划地图快照及其保守距离场。 + /// + /// 参数:environmentMap 为已经完成障碍物栅格化的环境图,坐标与分辨率单位均为 mm。 + /// 返回:不可变的 ;其世界查询使用 m,初始元数据由工厂随后分配。 + /// + public static PlanningGridMap Create(EnvironmentGridMap environmentMap) + { + if (!TryCreate(environmentMap, PlanningOperationBudget.Unlimited(CancellationToken.None), out PlanningGridMap map, out _)) + throw new InvalidOperationException("Unbounded planning-map adaptation unexpectedly stopped."); + return map; + } + + /// 使用共享预算创建规划快照;停止时不返回部分地图。 + internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, + out PlanningGridMap map, out PlanningOperationStopReason stopReason) + { + if (environmentMap == null) throw new ArgumentNullException(nameof(environmentMap)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + map = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + byte[] occupied = environmentMap.CopyCells(); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + if (!ObstacleDistanceField.TryCreate(occupied, environmentMap.Rows, environmentMap.Cols, + environmentMap.ResolutionMm / 1000d, budget, out ObstacleDistanceField field, out stopReason)) + return false; + map = new PlanningGridMap(environmentMap.Bounds, environmentMap.ResolutionMm, environmentMap.Rows, environmentMap.Cols, + occupied, field.CopyDistances(), 0, false, "Map metadata has not been assigned.", string.Empty, string.Empty); + stopReason = budget.GetStopReason(); + if (stopReason == PlanningOperationStopReason.None) return true; + map = null; + return false; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs new file mode 100644 index 0000000..06fe232 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 线程安全、容量为四的 LRU 缓存,分别复用精确输入结果和不可变占据数组。 +internal sealed class PlanningMapCache +{ + private const int Capacity = 4; + private readonly object _gate = new object(); + private readonly LinkedList _inputs = new LinkedList(); + private readonly LinkedList _occupancies = new LinkedList(); + + /// 按完整输入描述查询缓存。命中时返回原始快照与来源结果,并提升其最近使用顺序。 + public bool TryGetInput(string descriptor, out PlanningGridMap map, out IReadOnlyList sourceResults) + { + lock (_gate) + { + for (var node = _inputs.First; node != null; node = node.Next) + if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) + { map = node.Value.Map; sourceResults = node.Value.SourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return true; } + map = null; sourceResults = null; return false; + } + } + + /// 按占据哈希及逐格比较查询缓存。命中时返回共享数组的规范快照,供工厂创建新的元数据快照。 + public bool TryGetOccupancy(string hash, PlanningGridMap candidate, out PlanningGridMap canonical) + { + lock (_gate) + { + for (var node = _occupancies.First; node != null; node = node.Next) + if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(candidate)) + { canonical = node.Value.Map; _occupancies.Remove(node); _occupancies.AddFirst(node); return true; } + canonical = null; return false; + } + } + + /// 写入或更新精确输入缓存。参数 descriptor 为完整输入键,map 为不可变快照,sourceResults 为对应来源结果。 + public void AddInput(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) + { + lock (_gate) + { + for (var node = _inputs.First; node != null; node = node.Next) + if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) { node.Value.Map = map; node.Value.SourceResults = sourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return; } + _inputs.AddFirst(new InputEntry(descriptor, map, sourceResults)); + while (_inputs.Count > Capacity) _inputs.RemoveLast(); + } + } + + /// 写入占据缓存。参数 hash 为占据内容哈希,map 为包含可复用占据与距离数组的快照。 + public void AddOccupancy(string hash, PlanningGridMap map) + { + lock (_gate) + { + for (var node = _occupancies.First; node != null; node = node.Next) + if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(map)) + { _occupancies.Remove(node); _occupancies.AddFirst(node); return; } + _occupancies.AddFirst(new OccupancyEntry(hash, map)); + while (_occupancies.Count > Capacity) _occupancies.RemoveLast(); + } + } + + private sealed class InputEntry { public InputEntry(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) { Descriptor = descriptor; Map = map; SourceResults = sourceResults; } public string Descriptor { get; } public PlanningGridMap Map { get; set; } public IReadOnlyList SourceResults { get; set; } } + private sealed class OccupancyEntry { public OccupancyEntry(string hash, PlanningGridMap map) { Hash = hash; Map = map; } public string Hash { get; } public PlanningGridMap Map { get; } } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs new file mode 100644 index 0000000..a778d3b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 规划地图创建的终止状态。 +public enum PlanningMapBuildStatus +{ + /// 已成功创建可用的不可变地图快照。 + Success, + /// 输入、来源或地图构建失败。 + Failed, + /// 调用方在地图创建期间取消了操作。 + Cancelled, + /// 地图创建消耗了整次规划操作的总超时预算。 + TimedOut, +} + +/// 本次地图创建使用的缓存层级,反映快照或占据数组的复用方式。 +public enum PlanningMapCacheHit +{ + /// 未命中缓存;本次重新创建了占据图和距离场。 + None, + /// 完整输入命中;返回与上次完全相同的不可变地图对象。 + Input, + /// 占据内容命中;复用占据和距离数组,但生成新的快照元数据。 + Occupancy, +} + +/// +/// 规划地图创建的最终结果。 +/// +/// 返回: +/// - Succeeded 为 true 时 Map 可用;false 时通过 FailureReason 获取原因。 +/// - SourceResults 始终保留已处理来源的投影状态,便于诊断。 +/// +public sealed class PlanningMapBuildResult +{ + private PlanningMapBuildResult(PlanningMapBuildStatus status, string failureReason, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit, PlanningGridMap map) + { + Status = status; FailureReason = failureReason ?? string.Empty; SourceResults = sourceResults ?? Array.Empty(); CacheHit = cacheHit; Map = map; + } + /// 本次建图的显式终止状态;取消和超时不提供地图快照。 + public PlanningMapBuildStatus Status { get; } + /// 本次建图是否成功。true 表示 Status 为 Success 且 Map 不为空;false 时读取 FailureReason。 + public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } + /// 建图失败的可读诊断。成功时为空字符串。 + public string FailureReason { get; } + /// 每个障碍物来源的投影结果,按建图器排序后的来源顺序排列。 + public IReadOnlyList SourceResults { get; } + /// 本次调用的缓存复用层级,用于性能诊断,不影响地图正确性。 + public PlanningMapCacheHit CacheHit { get; } + /// 成功时返回的不可变规划地图;失败时为 null。 + public PlanningGridMap Map { get; } + internal static PlanningMapBuildResult Success(PlanningGridMap map, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Success, null, sourceResults, cacheHit, map); } + internal static PlanningMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Failed, reason, sourceResults, PlanningMapCacheHit.None, null); } + internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) + { + if (stopReason == PlanningOperationStopReason.Cancelled) + return new PlanningMapBuildResult(PlanningMapBuildStatus.Cancelled, "地图创建已取消。", sourceResults, PlanningMapCacheHit.None, null); + if (stopReason == PlanningOperationStopReason.TimedOut) + return new PlanningMapBuildResult(PlanningMapBuildStatus.TimedOut, "地图创建已超时。", sourceResults, PlanningMapCacheHit.None, null); + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs new file mode 100644 index 0000000..1dc7e04 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapFactory.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 规划地图的唯一公开创建入口。 +/// +/// 注意: +/// - 应在规划服务生命周期内长期复用同一实例,才能命中输入和占据两级缓存。 +/// - 本类只消费请求中的纯障碍物快照,不读取传感器、定位、UI 或系统时钟。 +/// +public sealed class PlanningMapFactory +{ + private readonly EnvironmentMapBuilder _builder = new EnvironmentMapBuilder(); + private readonly PlanningMapCache _cache = new PlanningMapCache(); + private readonly object _createGate = new object(); + private long _nextSnapshotId; + + /// + /// 创建规划地图快照。 + /// + /// 参数: + /// - request:完整建图请求;包含世界范围、栅格分辨率、障碍物来源和空图策略,不能为空。 + /// + /// 返回: + /// - PlanningMapBuildResult:成功时包含不可变 PlanningGridMap、每个来源的投影结果和缓存命中类型; + /// 失败时包含失败原因,不提供地图。 + /// + /// 注意: + /// - 完全相同的输入返回同一个快照对象;版本变化但占据相同则复用底层栅格并生成新快照编号。 + /// + public PlanningMapBuildResult Create(PlanningMapRequest request) + { + return Create(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); + } + + /// 使用共享预算创建地图;取消或超时时不发布快照或缓存条目。 + internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget) + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + + bool enteredCreateGate = false; + try + { + while (!Monitor.TryEnter(_createGate, 16)) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + } + enteredCreateGate = true; + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + if (request == null || request.Bounds == null) return PlanningMapBuildResult.Failure("Planning map request and bounds are required.", null); + if (request.ObstacleSources == null) return PlanningMapBuildResult.Failure("Obstacle source collection is required.", null); + string requestKey = BuildRequestKey(request); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + if (_cache.TryGetInput(requestKey, out PlanningGridMap exactMap, out IReadOnlyList cachedResults)) + { + stopReason = budget.GetStopReason(); + return stopReason == PlanningOperationStopReason.None + ? PlanningMapBuildResult.Success(exactMap, cachedResults, PlanningMapCacheHit.Input) + : PlanningMapBuildResult.Stopped(stopReason, cachedResults); + } + + EnvironmentMapBuildResult environment = _builder.Build( + new MapBuildRequest { Bounds = request.Bounds, ResolutionMm = request.ResolutionMm, ObstacleSources = request.ObstacleSources }, budget); + if (!environment.Succeeded) + return environment.StopReason == PlanningOperationStopReason.None + ? PlanningMapBuildResult.Failure(environment.FailureReason, environment.SourceResults) + : PlanningMapBuildResult.Stopped(environment.StopReason, environment.SourceResults); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + string descriptor = BuildInputDescriptor(request, environment.SourceResults); + string inputFingerprint = Sha256(descriptor); + int applied = environment.SourceResults.Count(x => x.Status == ObstacleSourceStatus.Applied); + bool ready = applied > 0 || request.AllowExplicitEmptyMap; + string blockReason = ready ? string.Empty : "No source supplied obstacle geometry; set AllowExplicitEmptyMap only when an intentionally empty map is safe."; + if (!PlanningMapAdapter.TryCreate(environment.Map, budget, out PlanningGridMap candidate, out stopReason)) + return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + if (!TryComputeOccupancyHash(candidate, budget, out string occupancyHash, out stopReason)) + return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + + PlanningGridMap map; + PlanningMapCacheHit cacheHit; + if (_cache.TryGetOccupancy(occupancyHash, candidate, out PlanningGridMap canonical)) + { + map = canonical.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); + cacheHit = PlanningMapCacheHit.Occupancy; + } + else + { + map = candidate.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + _cache.AddOccupancy(occupancyHash, map); + cacheHit = PlanningMapCacheHit.None; + } + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + _cache.AddInput(requestKey, map, environment.SourceResults); + return PlanningMapBuildResult.Success(map, environment.SourceResults, cacheHit); + } + finally + { + if (enteredCreateGate) Monitor.Exit(_createGate); + } + } + + private long NextSnapshotId() { return Interlocked.Increment(ref _nextSnapshotId); } + + private static string BuildInputDescriptor(PlanningMapRequest request, IReadOnlyList results) + { + var builder = new StringBuilder(); + AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); + var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); + for (int i = 0; i < sources.Length; i++) + { + IMapObstacleSource source = sources[i]; + builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); + if (i < results.Count) AppendProjection(builder, results[i]); + } + return builder.ToString(); + } + private static string BuildRequestKey(PlanningMapRequest request) + { + var builder = new StringBuilder(); + AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); + foreach (IMapObstacleSource source in request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal)) + builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); + return builder.ToString(); + } + private static void AppendProjection(StringBuilder builder, ObstacleProjectionResult result) + { + builder.Append('|').Append((int)result.Status); + for (int index = 0; index < result.Obstacles.Count; index++) + { + var circle = result.Obstacles[index] as CircleObstacle; + if (circle != null) { builder.Append("|C"); AppendFloat(builder, circle.CenterX); AppendFloat(builder, circle.CenterY); AppendFloat(builder, circle.RadiusMm); continue; } + var rectangle = result.Obstacles[index] as AxisAlignedRectangleObstacle; + if (rectangle != null) { builder.Append("|R"); AppendFloat(builder, rectangle.XMin); AppendFloat(builder, rectangle.XMax); AppendFloat(builder, rectangle.YMin); AppendFloat(builder, rectangle.YMax); } + } + } + private static void AppendFloat(StringBuilder builder, float value) { builder.Append(BitConverter.ToInt32(BitConverter.GetBytes(value), 0).ToString("X8")); } + private static bool TryComputeOccupancyHash(PlanningGridMap map, PlanningOperationBudget budget, out string occupancyHash, + out PlanningOperationStopReason stopReason) + { + byte[] occupied = map.CopyOccupied(); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + using (var sha = SHA256.Create()) + { + const int blockLength = 4096; + int offset = 0; + while (occupied.Length - offset > blockLength) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + sha.TransformBlock(occupied, offset, blockLength, occupied, offset); + offset += blockLength; + } + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + byte[] hash = sha.TransformFinalBlock(occupied, offset, occupied.Length - offset); + occupancyHash = BitConverter.ToString(hash).Replace("-", string.Empty); + stopReason = PlanningOperationStopReason.None; + return true; + } + } + private static string Sha256(string text) + { + using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs new file mode 100644 index 0000000..26a6822 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/PlanningMapRequest.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 规划地图的完整建图输入。 +/// +/// 注意: +/// - 此对象只描述地图内容,不包含车辆、起终点、PNG 或终端日志等调试参数。 +/// - 障碍物来源快照变化时,调用方必须更新对应的 SourceVersion。 +/// +public sealed class PlanningMapRequest +{ + /// + /// 地图世界边界。 + /// 单位:mm;边界采用左闭右开范围,由 MapBoundsMm 的 XMin、XMax、YMin、YMax 定义。 + /// + public MapBoundsMm Bounds { get; set; } + /// + /// 栅格边长。 + /// 单位:mm;当前 MapBoundsMm 允许的范围为 20 至 200 mm。 + /// + public float ResolutionMm { get; set; } + /// + /// 参与建图的统一障碍物来源。 + /// 参数:列表中的每个来源必须具有唯一 ID 和非负版本号;可为空列表。 + /// + public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); + /// + /// 是否明确允许没有任何障碍物几何的地图参与规划。 + /// 返回语义:false 时,隐式空图会被标记为不可规划;true 时空图可用于规划。 + /// + public bool AllowExplicitEmptyMap { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/README.md b/.task8-sweep/ParkrobTrajplanner/Map/README.md new file mode 100644 index 0000000..1d1861d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/README.md @@ -0,0 +1,367 @@ +# Map 模块说明 + +`Map` 为粗路径规划提供只读、可复用的规划地图快照。它只负责把外部障碍物投影并栅格化,生成占据图和保守障碍距离场;不读取传感器、定位、UI 或系统时钟,也不写入 AMR 自身占据和安全外扩。 + +规划器应长期持有一个 `PlanningMapFactory`,通过一次 `Create` 调用取得 `PlanningGridMap`。 + +## 文件结构 + +```text +Map/ +├── README.md # 本模块说明:结构、数据流、单位和调用方式 +├── PlanningMapRequest.cs # 公开建图输入:边界、分辨率、障碍来源、空图策略 +├── PlanningMapBuildResult.cs # 公开建图输出:状态、地图、来源结果、失败原因、缓存命中类型 +├── PlanningMapFactory.cs # 唯一公开建图门面;负责两级缓存和快照编号 +├── Core/ +│ ├── MapBoundsMm.cs # 世界地图范围及行列尺寸计算 +│ ├── MapBuildRequest.cs # EnvironmentMapBuilder 的内部建图输入 +│ ├── EnvironmentMapBuildResult.cs # 环境栅格构建结果 +│ ├── EnvironmentGridMap.cs # 构建期可写的环境占据栅格 +│ └── EnvironmentMapBuilder.cs # 汇总障碍来源并事务性创建环境图 +├── Obstacles/ +│ ├── IMapObstacle.cs # 世界坐标障碍物几何契约 +│ ├── CircleObstacle.cs # 圆形障碍物几何 +│ ├── AxisAlignedRectangleObstacle.cs # 与坐标轴平行的矩形障碍物几何 +│ └── MapObstacleRasterizer.cs # 唯一允许写入环境栅格的障碍物栅格化器 +├── Sources/ +│ ├── IMapObstacleSource.cs # 统一障碍来源接口 +│ ├── ObstacleSourceStatus.cs # 来源投影状态:已应用、空、不可用、无效 +│ ├── ObstacleProjectionResult.cs # 单个来源的世界几何和诊断结果 +│ ├── ManualObstacleSource.cs # 手工输入的圆形/矩形障碍来源 +│ ├── TwoLegProjectionInput.cs # 检测时刻的 TwoLeg 纯数据快照 +│ ├── TwoLegObstacleProjector.cs # 将 TwoLeg 局部坐标投影为世界坐标圆障碍物 +│ └── TwoLegObstacleSource.cs # 将 TwoLeg 快照包装为统一障碍来源 +├── Planning/ +│ ├── PlanningGridMap.cs # 不可变规划快照;规划查询使用米 +│ ├── PlanningMapAdapter.cs # 环境图到规划快照与距离场的适配器 +│ ├── EuclideanDistanceTransform.cs # 二值栅格的精确平方欧氏距离变换 +│ ├── ObstacleDistanceField.cs # 对规划器暴露的保守障碍净距 +│ └── PlanningMapCache.cs # 容量为 4 的输入/占据两级 LRU 缓存 +└── Test/ + ├── MovementTest.MapTest.cs # Clumsy 手工建图测试入口与终端调试开关 + └── Visualization/ + ├── PlanningMapImageExportRequest.cs # PNG 导出输入:规划快照和输出目录 + ├── PlanningMapImageExportResult.cs # PNG 导出状态、路径、尺寸和诊断 + ├── PlanningMapImageExporter.cs # 可选 PNG 导出门面和输出保护 + ├── PlanningMapImageRenderer.cs # 只读快照到 RGBA 像素的渲染器 + └── ValidatedPngWriter.cs # 写入并校验 PNG 结构和 CRC +``` + +## 建图数据流 + +```text +PlanningMapRequest + │ + ▼ +IMapObstacleSource.ProjectToWorld() + │ 输出世界坐标的圆形或矩形几何 + ▼ +EnvironmentMapBuilder + MapObstacleRasterizer + │ 写入构建期 EnvironmentGridMap + ▼ +PlanningMapAdapter + ObstacleDistanceField + │ 生成占据数组和保守距离数组 + ▼ +PlanningGridMap +``` + +具体规则: + +1. 调用者准备 `PlanningMapRequest` 和一个或多个 `IMapObstacleSource`。 +2. 每个来源通过 `ProjectToWorld()` 输出世界坐标几何;Map 核心不主动读取 TwoLeg、定位或其他传感器。 +3. `EnvironmentMapBuilder` 按来源 ID 排序,必需来源失败则整个建图失败;可选来源失败只保留诊断状态。 +4. `MapObstacleRasterizer` 是唯一写入 `EnvironmentGridMap` 占据格的组件。 +5. `PlanningMapAdapter` 生成不可变 `PlanningGridMap`,并附带保守障碍距离场。 +6. `PlanningMapFactory` 返回最终快照及来源投影结果;粗路径规划只应消费这个快照。 + +## 构建状态与停止 + +`PlanningMapBuildResult.Status` 的类型为 `PlanningMapBuildStatus`: + +- `Success`:成功发布不可变 `PlanningGridMap`; +- `Failed`:输入、来源或常规建图失败,读取 `FailureReason`; +- `Cancelled`:调用方取消了带预算的建图,`Map` 为 `null`,不会写入缓存; +- `TimedOut`:建图耗尽调用方的总超时预算,`Map` 为 `null`,不会写入缓存。 + +公开的 `PlanningMapFactory.Create(request)` 保持兼容且不设置时间限制;粗规划门面使用内部预算入口,使取消和超时能够覆盖地图创建、距离场和后续搜索。 + +## 坐标与单位 + +- 环境地图边界、障碍物几何、TwoLeg 投影输入均使用世界坐标,单位为 **mm**。 +- `MapBoundsMm` 范围采用左闭右开:`[XMin, XMax) × [YMin, YMax)`;最大边界不属于地图。 +- `EnvironmentGridMap` 查询使用 mm;`PlanningGridMap` 的世界查询使用 **m**。 +- 行 `row` 对应 Y 方向,列 `col` 对应 X 方向,存储顺序为行主序 `row * Cols + col`。 +- `PlanningGridMap` 的越界位置按占据处理,障碍净距返回 0 m。 +- Map 不做车辆自身占据或安全外扩;车辆外形与安全裕度由后续碰撞检测负责。 + +## 最小调用示例 + +```csharp +var mapFactory = new PlanningMapFactory(); // 长期持有,不要每次规划重新创建 + +IMapObstacle[] obstacles = +{ + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), +}; + +var request = new PlanningMapRequest +{ + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, obstacles), + }, + AllowExplicitEmptyMap = false, +}; + +PlanningMapBuildResult result = mapFactory.Create(request); +if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + throw new InvalidOperationException(result.FailureReason); + +PlanningGridMap map = result.Map; // 交给粗路径规划器 +``` + +真实 TwoLeg 数据应由上层检测模块在检测时刻构造成 `TwoLegProjectionInput`,再交给 `TwoLegObstacleSource`。不要让 Map 模块主动读取检测器或定位器。 + +## 缓存与版本 + +`PlanningMapFactory` 内部有容量为 4 的两级 LRU 缓存: + +- **输入命中(Input)**:边界、分辨率、空图策略、来源 ID、`SourceVersion` 和必需性均相同,直接返回同一个 `PlanningGridMap` 对象。 +- **占据命中(Occupancy)**:来源版本变化,但最终占据栅格相同,复用不可变的占据/距离数组,并颁发新的 `SnapshotId`。 +- **未命中(None)**:栅格内容变化,重新生成规划快照。 + +因此,障碍来源的快照内容发生变化时,调用方必须增加其 `SourceVersion`。未递增版本会错误复用旧地图;仅修改日志、PNG 开关、起终点或车辆参数不应改变地图版本。 + +## 测试与调试 + +- `Test/MovementTest.MapTest.cs` 是手工 Clumsy 测试入口。文件顶部可设置地图范围、分辨率、TwoLeg 测试快照、`EnableTerminalDebugLog` 和 `SavePng`。 +- `PlanningMapImageExporter` 仅在 `SavePng` 开启时输出 PNG;它只读取 `PlanningGridMap`,不参与建图、缓存键或规划结果。 +- 自动检查脚本位于 `ClumsyPilot/tests`:工具、工厂、适配器、PNG 和 MapTest 配置分别有独立验证脚本。 +- 旧版 `Occupancygird_Map/Map_test/TrapMapImageExporter.cs` 与 `MovementTest.Trapmaptest.cs` 仍保留作历史对照;它们不是新粗路径规划的运行时地图入口。 + +## 详细使用指南 + +本节说明调用方如何从“障碍物数据”逐步得到可交给粗路径规划器的 `PlanningGridMap`。新地图的唯一创建入口是: + +```csharp +PlanningMapBuildResult result = mapFactory.Create(request); +``` + +其中 `mapFactory` 是长期持有的 `PlanningMapFactory`,`request` 是本次建图输入,`result.Map` 是成功时的只读规划地图。 + +### 第 1 步:长期创建地图工厂 + +地图工厂内部维护容量为 4 的缓存,因此不要在每次规划前重新创建它。应把它作为规划服务或 MovementTest 的字段长期保存: + +```csharp +using MultiWheelC.TrajectoryPlanning.Mapping; + +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); +``` + +### 第 2 步:准备手工障碍物 + +圆形和轴对齐矩形都实现 `IMapObstacle`。障碍物的坐标是**世界坐标**,单位都是 **mm**。 + +```csharp +IMapObstacle[] manualObstacles = +{ + // 参数依次为:X最小值、X最大值、Y最小值、Y最大值,单位均为 mm。 + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + + // 参数依次为:圆心X、圆心Y、半径,单位均为 mm。 + new CircleObstacle(3600f, 1200f, 180f), +}; +``` + +目前 Map 只负责“环境障碍物”。不要在这里添加 AMR 自身外形,也不要在圆半径或矩形尺寸中叠加安全裕度;车辆外形与安全距离由粗路径的碰撞检查处理。 + +### 第 3 步:包装为统一障碍来源 + +所有障碍物必须通过 `IMapObstacleSource` 进入地图。手工障碍使用 `ManualObstacleSource`: + +```csharp +var manualSource = new ManualObstacleSource( + sourceId: "manual", + sourceVersion: 1L, + isRequired: true, + obstacles: manualObstacles); +``` + +参数意义如下: + +- `sourceId`:来源的唯一名称;同一次请求内不能重复。 +- `sourceVersion`:来源快照版本。障碍物位置、数量、半径或尺寸变化后,必须递增。 +- `isRequired`:`true` 表示来源无效时整次建图失败;`false` 表示仅记录该来源状态并继续建图。 +- `obstacles`:当前时刻的不可变障碍物集合。 + +例如障碍物内容变化后,应创建带新版本号的来源: + +```csharp +var changedManualSource = new ManualObstacleSource( + "manual", + 2L, // 1L 变为 2L,通知工厂地图输入已改变 + true, + changedObstacles); +``` + +### 第 4 步:可选地加入 TwoLeg 障碍物 + +Map 不主动调用 TwoLeg 检测器。上层检测模块应在检测时刻取得 AMR 世界位姿和两腿局部坐标,构造成 `TwoLegProjectionInput`: + +```csharp +var twoLegInput = new TwoLegProjectionInput( + hasDetection: true, + + // 检测时 AMR 的世界位姿:位置单位 mm,航向单位 rad。 + detectionWorldX: 1000f, + detectionWorldY: 2000f, + detectionHeadingRadians: 0d, + + // 两条腿相对于检测时 AMR 位姿的局部坐标,单位 mm。 + firstLocalX: 300f, + firstLocalY: 150f, + secondLocalX: 300f, + secondLocalY: -150f, + + // 每条腿最终投影为圆形障碍物的半径,单位 mm。 + radiusMm: 80f, + diagnostic: "TwoLeg 检测快照"); + +var twoLegSource = new TwoLegObstacleSource( + sourceId: "two-leg", + sourceVersion: 5L, + isRequired: false, + input: twoLegInput); +``` + +没有检测结果时仍可传入空快照: + +```csharp +var noTwoLegInput = new TwoLegProjectionInput( + false, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, + "当前没有 TwoLeg 检测结果"); +``` + +`hasDetection` 为 `false` 时,该来源会返回“空”结果,不会将零坐标当作障碍物。若 TwoLeg 只是可选信息,建议 `isRequired` 设置为 `false`。 + +### 第 5 步:创建建图请求 + +边界与分辨率仍使用 **mm**。范围采用左闭右开,例如 `XMax = 6000f` 时,`x = 6000f` 不属于地图。 + +```csharp +var request = new PlanningMapRequest +{ + // 世界范围:[0, 6000) × [0, 4000),单位 mm。 + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + + // 每格 50 mm。 + ResolutionMm = 50f, + + ObstacleSources = new IMapObstacleSource[] + { + manualSource, + twoLegSource, + }, + + // false:没有任何有效障碍物时,地图不能直接交给规划器。 + // true:调用方明确确认空地图安全时,才允许空图参与规划。 + AllowExplicitEmptyMap = false, +}; +``` + +### 第 6 步:创建地图并处理失败 + +```csharp +PlanningMapBuildResult result = _mapFactory.Create(request); + +if (!result.Succeeded) +{ + Console.WriteLine("建图失败:" + result.FailureReason); + return; +} + +if (result.Map == null || !result.Map.PlanningReady) +{ + Console.WriteLine("地图不能用于规划:" + result.Map?.PlanningBlockReason); + return; +} + +PlanningGridMap planningMap = result.Map; +``` + +此处必须同时检查: + +- `Succeeded`:请求、来源和栅格构建过程是否成功; +- `Map != null`:是否产出了规划快照; +- `PlanningReady`:是否允许把该快照交给规划器。隐式空图会在这一项被拦截。 + +需要查看每个来源是否成功投影时,读取 `result.SourceResults`;需要观察缓存效果时,读取 `result.CacheHit`。 + +```csharp +Console.WriteLine( + "快照编号=" + planningMap.SnapshotId + + ",缓存=" + result.CacheHit + + ",栅格=" + planningMap.Cols + "×" + planningMap.Rows); +``` + +### 第 7 步:交给粗路径规划器查询 + +`PlanningGridMap` 是不可变对象,可以安全地作为一次规划任务的输入。注意:它的世界查询坐标单位已经变成 **m**,而不是 mm。 + +```csharp +// 查询 (2.5 m, 1.0 m) 是否位于占据格;越界也会返回 true。 +bool occupied = planningMap.IsOccupiedWorld(2.5d, 1.0d); + +// 查询该位置到最近障碍物的保守净距,单位 m;越界返回 0 m。 +double clearanceMeters = + planningMap.GetConservativeObstacleDistanceMeters(2.5d, 1.0d); +``` + +粗路径规划器应只使用 `PlanningGridMap` 的占据与距离查询,不应直接修改或重建其中的栅格。 + +### 完整实例 + +```csharp +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + +private PlanningGridMap CreateMapForCoarsePlanning() +{ + IMapObstacle[] obstacles = + { + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), + }; + + var manualSource = new ManualObstacleSource("manual", 1L, true, obstacles); + + var request = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] { manualSource }, + AllowExplicitEmptyMap = false, + }; + + PlanningMapBuildResult result = _mapFactory.Create(request); + if (!result.Succeeded) + throw new InvalidOperationException("建图失败:" + result.FailureReason); + if (result.Map == null || !result.Map.PlanningReady) + throw new InvalidOperationException("地图不可规划:" + result.Map?.PlanningBlockReason); + + return result.Map; +} +``` + +### 常见错误 + +| 情况 | 原因 | 处理方式 | +| --- | --- | --- | +| 改了障碍物但地图仍复用旧快照 | 未递增 `SourceVersion` | 障碍内容每次变化后增加该来源版本号 | +| 规划查询位置总是越界 | 将 mm 坐标传给了 `PlanningGridMap` | 规划查询前将 mm 除以 1000 转为 m | +| 地图创建成功但不能规划 | 未明确允许空图且没有有效障碍物 | 补充有效来源,或确认安全后设置 `AllowExplicitEmptyMap = true` | +| TwoLeg 出现在错误位置 | 未使用检测时刻位姿,或混用了 mm 与 m | 用检测时刻的世界位姿和局部 mm 坐标创建快照 | +| 缓存总是未命中 | 每次都新建 `PlanningMapFactory`,或版本号无意义变化 | 长期复用工厂;仅在来源内容实际变化时递增版本 | diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs new file mode 100644 index 0000000..ca0312f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs @@ -0,0 +1,22 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 提供一次性障碍物快照的纯数据来源。 +/// +/// 注意: 不得读取传感器、定位、UI 或时钟;调用前应由上层采集并封装完整快照。 +/// +public interface IMapObstacleSource +{ + /// 来源的稳定唯一标识。不能为空;同一次建图请求中不得重复。 + string SourceId { get; } + /// 来源快照版本号,必须非负。快照几何或有效性变化时必须递增,以使输入缓存失效。 + long SourceVersion { get; } + /// 该来源是否必需。true 时不可用或无效会使整张地图构建失败;false 时只记录状态。 + bool IsRequired { get; } + /// + /// 将已采集的快照投影为世界障碍物几何。 + /// + /// 返回:世界坐标、单位 mm 的障碍物及其状态;不得访问任何实时外部状态。 + /// + ObstacleProjectionResult ProjectToWorld(); +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs new file mode 100644 index 0000000..e4b9334 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 直接提供世界 mm 几何列表的手工障碍物快照来源。 +public sealed class ManualObstacleSource : IMapObstacleSource +{ + private readonly IReadOnlyList _obstacles; + /// + /// 创建手工障碍物来源。 + /// + /// 参数:sourceId 为唯一来源标识;sourceVersion 为非负快照版本;isRequired 指示失败是否阻断建图;obstacles 为世界 mm 几何列表。 + /// 注意:几何内容变化时,调用方必须同步提高 sourceVersion。 + /// + public ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles) + { + SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _obstacles = obstacles; + } + /// 该快照的唯一来源标识。 + public string SourceId { get; } + /// 该快照的版本号;内容变化时必须递增。 + public long SourceVersion { get; } + /// true 表示无效来源会阻断建图;false 表示只记录来源状态。 + public bool IsRequired { get; } + /// + /// 返回手工障碍物的世界几何。 + /// + /// 返回:列表为空时为 Empty;列表为空引用或含无效几何时为 Invalid;其余情况为 Applied。 + /// + public ObstacleProjectionResult ProjectToWorld() + { + if (_obstacles == null) return ObstacleProjectionResult.Invalid("Manual obstacle collection is null."); + if (_obstacles.Count == 0) return ObstacleProjectionResult.Empty("Manual obstacle collection is empty."); + for (int i = 0; i < _obstacles.Count; i++) + if (_obstacles[i] == null || !_obstacles[i].IsValid) return ObstacleProjectionResult.Invalid("Manual obstacle geometry is invalid."); + return ObstacleProjectionResult.Applied(_obstacles, "Manual obstacle snapshot applied."); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs new file mode 100644 index 0000000..b429f26 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 障碍物来源的一次投影结果,包含状态、世界 mm 几何和可选诊断信息。 +public sealed class ObstacleProjectionResult +{ + private ObstacleProjectionResult(ObstacleSourceStatus status, IReadOnlyList obstacles, string diagnostic) + { + Status = status; Obstacles = obstacles ?? Array.Empty(); Diagnostic = diagnostic ?? string.Empty; + } + /// 投影状态。只有 的几何会被栅格化。 + public ObstacleSourceStatus Status { get; } + /// 世界 mm 坐标的障碍物列表。非 Applied 状态时为空列表,不为 null。 + public IReadOnlyList Obstacles { get; } + /// 用于日志和诊断的说明文本;未提供时为空字符串。 + public string Diagnostic { get; } + /// 创建成功投影结果。参数 obstacles 为有效世界 mm 几何;diagnostic 为可选诊断文本。返回的状态为 Applied。 + public static ObstacleProjectionResult Applied(IReadOnlyList obstacles, string diagnostic = null) + { + return new ObstacleProjectionResult(ObstacleSourceStatus.Applied, obstacles, diagnostic); + } + /// 创建空投影结果。参数 diagnostic 为可选原因;返回状态为 Empty,障碍物列表为空。 + public static ObstacleProjectionResult Empty(string diagnostic = null) { return new ObstacleProjectionResult(ObstacleSourceStatus.Empty, null, diagnostic); } + /// 创建不可用投影结果。参数 diagnostic 应说明快照缺失原因;返回状态为 Unavailable。 + public static ObstacleProjectionResult Unavailable(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Unavailable, null, diagnostic); } + /// 创建无效投影结果。参数 diagnostic 应说明数据或几何错误;返回状态为 Invalid。 + public static ObstacleProjectionResult Invalid(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Invalid, null, diagnostic); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs new file mode 100644 index 0000000..f71161a --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 一个障碍物来源投影后的状态,供建图结果和调试日志判断。 +public enum ObstacleSourceStatus +{ + /// 成功得到一个或多个有效的世界 mm 障碍物,并会被栅格化。 + Applied, + /// 来源有效但本次没有障碍物;不会写入任何栅格。 + Empty, + /// 来源快照不可取得;若来源必需则整图构建失败。 + Unavailable, + /// 来源数据或几何无效;若来源必需则整图构建失败。 + Invalid, +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs new file mode 100644 index 0000000..50afab3 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将检测局部坐标中的两条腿转换为世界 mm 坐标圆形障碍物。 +public static class TwoLegObstacleProjector +{ + /// + /// 投影一份 TwoLeg 快照。 + /// + /// 参数:input 为检测时刻采集的纯数据快照,可为 null。 + /// 返回:null 为 Unavailable;未检测到目标为 Empty;数值无效为 Invalid;成功时为含两个圆形障碍物的 Applied。 + /// 注意:本方法只读取 input,不访问传感器、定位、UI 或时钟。 + /// + public static ObstacleProjectionResult Project(TwoLegProjectionInput input) + { + if (input == null) return ObstacleProjectionResult.Unavailable("TwoLeg snapshot is unavailable."); + if (!input.HasDetection) return ObstacleProjectionResult.Empty(input.Diagnostic); + if (!input.IsValid) return ObstacleProjectionResult.Invalid("TwoLeg snapshot contains invalid values."); + CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, + input.FirstLocalX, input.FirstLocalY, out double firstX, out double firstY); + CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, + input.SecondLocalX, input.SecondLocalY, out double secondX, out double secondY); + var obstacles = new List + { + new CircleObstacle((float)firstX, (float)firstY, input.RadiusMm), + new CircleObstacle((float)secondX, (float)secondY, input.RadiusMm), + }; + return ObstacleProjectionResult.Applied(obstacles, input.Diagnostic); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs new file mode 100644 index 0000000..206fd4b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs @@ -0,0 +1,24 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将一个 快照包装为统一障碍物来源。 +public sealed class TwoLegObstacleSource : IMapObstacleSource +{ + private readonly TwoLegProjectionInput _input; + /// + /// 创建 TwoLeg 障碍物来源。 + /// + /// 参数:sourceId 为唯一标识;sourceVersion 为快照版本,输入内容改变时必须递增;isRequired 指示失败是否阻断建图;input 为检测时刻快照,可为空。 + /// + public TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input) + { + SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _input = input; + } + /// 该快照来源的唯一标识。 + public string SourceId { get; } + /// 快照版本号;输入内容变化时必须递增。 + public long SourceVersion { get; } + /// true 时投影不可用或无效会阻断建图。 + public bool IsRequired { get; } + /// 投影保存的 TwoLeg 快照。返回世界 mm 圆形障碍物,且不访问实时检测、定位、UI 或时钟。 + public ObstacleProjectionResult ProjectToWorld() { return TwoLegObstacleProjector.Project(_input); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs new file mode 100644 index 0000000..bb1ed94 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs @@ -0,0 +1,51 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// TwoLeg 检测时刻的纯输入快照。 +/// +/// 单位:位置和两腿局部坐标均为 mm,航向为弧度。该对象不读取检测器或定位模块,确保投影可重复。 +/// +public sealed class TwoLegProjectionInput +{ + /// + /// 创建 TwoLeg 投影快照。 + /// + /// 参数:hasDetection 表示当前是否检测到目标;detectionWorldX/Y 为检测坐标系原点的世界 mm 坐标;detectionHeadingRadians 为其相对世界坐标系的航向;firstLocalX/Y、secondLocalX/Y 为两腿在检测局部坐标系中的 mm 坐标;radiusMm 为每条腿投影圆半径;diagnostic 为可选诊断文本。 + /// 注意:hasDetection 为 false 时其余几何参数不会参与有效性校验。 + /// + public TwoLegProjectionInput(bool hasDetection, float detectionWorldX, float detectionWorldY, + double detectionHeadingRadians, float firstLocalX, float firstLocalY, + float secondLocalX, float secondLocalY, float radiusMm, string diagnostic = null) + { + HasDetection = hasDetection; DetectionWorldX = detectionWorldX; DetectionWorldY = detectionWorldY; + DetectionHeadingRadians = detectionHeadingRadians; FirstLocalX = firstLocalX; FirstLocalY = firstLocalY; + SecondLocalX = secondLocalX; SecondLocalY = secondLocalY; RadiusMm = radiusMm; Diagnostic = diagnostic ?? string.Empty; + } + /// 检测是否存在。false 时投影结果为 Empty。 + public bool HasDetection { get; } + /// 检测局部坐标系原点的世界 X 坐标,单位 mm。 + public float DetectionWorldX { get; } + /// 检测局部坐标系原点的世界 Y 坐标,单位 mm。 + public float DetectionWorldY { get; } + /// 检测局部坐标系相对世界坐标系的航向,单位弧度。 + public double DetectionHeadingRadians { get; } + /// 第一条腿在检测局部坐标系的 X 坐标,单位 mm。 + public float FirstLocalX { get; } + /// 第一条腿在检测局部坐标系的 Y 坐标,单位 mm。 + public float FirstLocalY { get; } + /// 第二条腿在检测局部坐标系的 X 坐标,单位 mm。 + public float SecondLocalX { get; } + /// 第二条腿在检测局部坐标系的 Y 坐标,单位 mm。 + public float SecondLocalY { get; } + /// 投影为圆形障碍物时的半径,单位 mm。 + public float RadiusMm { get; } + /// 上游检测阶段提供的可选诊断文本;未提供时为空字符串。 + public string Diagnostic { get; } + /// 未检测到目标时为 true;检测到目标时,所有数值有限且半径非负才为 true。 + public bool IsValid + { + get { return !HasDetection || (NumericGuard.IsFinite(DetectionWorldX) && NumericGuard.IsFinite(DetectionWorldY) && NumericGuard.IsFinite(DetectionHeadingRadians) && NumericGuard.IsFinite(FirstLocalX) && NumericGuard.IsFinite(FirstLocalY) && NumericGuard.IsFinite(SecondLocalX) && NumericGuard.IsFinite(SecondLocalY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f); } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs new file mode 100644 index 0000000..5a8eb2e --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using CommonUsage.Mathematics; +using FundamentalLib; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC; + +/// +/// 用于手工调试的 Clumsy 地图测试入口,只通过公开的 创建快照。 +/// +/// 注意:终端日志和 PNG 开关只影响调试输出,不参与建图输入、地图内容或缓存键。 +/// +[MovementTest(name = "规划地图快照测试V1")] +public sealed class PlanningMapTest : MovementTest +{ + private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + private const bool EnableTerminalDebugLog = true; + private const bool SavePng = true; + private const float MapXMinMm = 0f; + private const float MapXMaxMm = 6000f; + private const float MapYMinMm = 0f; + private const float MapYMaxMm = 4000f; + private const float GridResolutionMm = 50f; + private const bool AllowExplicitEmptyMap = false; + private const bool EnableTwoLegSnapshot = false; + + /// + /// 执行一次配置好的地图创建与调试输出。 + /// + /// 返回:无。测试使用本文件顶部的地图范围、分辨率、障碍物来源、日志和 PNG 开关;不会接入实时 TwoLeg 检测。 + /// + public override void Test() + { + IMapObstacleSource[] sources = CreateObstacleSources(); + Log("========== 规划地图创建开始 =========="); + Log("参数:地图范围=(" + MapXMinMm + "," + MapXMaxMm + ")x(" + MapYMinMm + "," + MapYMaxMm + + ")mm,分辨率=" + GridResolutionMm + "mm,允许空图=" + AllowExplicitEmptyMap + ",导出图片=" + SavePng); + Log("障碍物来源:" + FormatSources(sources)); + var result = _mapFactory.Create(new PlanningMapRequest + { + Bounds = new MapBoundsMm(MapXMinMm, MapXMaxMm, MapYMinMm, MapYMaxMm), + ResolutionMm = GridResolutionMm, + ObstacleSources = sources, + AllowExplicitEmptyMap = AllowExplicitEmptyMap, + }); + Log("投影结果:" + FormatProjectionResults(sources, result.SourceResults)); + if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + { + Log("规划地图创建失败:" + result.FailureReason); + Log("========== 规划地图创建结束 =========="); + return; + } + Log("地图快照:编号=" + result.Map.SnapshotId + ",缓存=" + FormatCacheHit(result.CacheHit) + ",栅格=" + result.Map.Cols + "x" + result.Map.Rows + + ",可规划=" + result.Map.PlanningReady + ",指纹=" + result.Map.InputFingerprint); + PlanningMapImageExportResult image = PlanningMapImageExporter.ExportIfEnabled(SavePng, + new PlanningMapImageExportRequest { Map = result.Map, OutputRootDirectory = Environment.CurrentDirectory }); + Log("图片导出:" + FormatImageResult(image)); + Log("========== 规划地图创建完成 =========="); + } + + private static IMapObstacleSource[] CreateObstacleSources() + { + IMapObstacle[] manualObstacles = + { + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), + }; + return new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, manualObstacles), + new TwoLegObstacleSource("two-leg", EnableTwoLegSnapshot ? 1L : 0L, false, + new TwoLegProjectionInput(EnableTwoLegSnapshot, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, "No current TwoLeg snapshot.")), + }; + } + + private static string FormatSources(IReadOnlyList sources) + { + return string.Join(";", sources.Select(source => source.SourceId + "(版本=" + source.SourceVersion + ",必需=" + source.IsRequired + ")")); + } + + private static string FormatProjectionResults(IReadOnlyList sources, IReadOnlyList results) + { + IMapObstacleSource[] sortedSources = sources.OrderBy(source => source.SourceId, StringComparer.Ordinal).ToArray(); + return string.Join(";", results.Select((result, index) => sortedSources[index].SourceId + "=" + FormatSourceStatus(result.Status) + "(" + result.Obstacles.Count + " 个障碍物)")); + } + + private static string FormatSourceStatus(ObstacleSourceStatus status) + { + switch (status) + { + case ObstacleSourceStatus.Applied: return "已应用"; + case ObstacleSourceStatus.Empty: return "空"; + case ObstacleSourceStatus.Unavailable: return "不可用"; + case ObstacleSourceStatus.Invalid: return "无效"; + default: return "未知"; + } + } + + private static string FormatCacheHit(PlanningMapCacheHit cacheHit) + { + switch (cacheHit) + { + case PlanningMapCacheHit.Input: return "输入命中"; + case PlanningMapCacheHit.Occupancy: return "占据图命中"; + default: return "未命中"; + } + } + + private static string FormatImageResult(PlanningMapImageExportResult image) + { + if (image.Skipped) return "未启用"; + if (image.Saved) return "已保存:" + image.FilePath; + return "失败:" + image.Message; + } + + private static void Log(string message) + { + if (EnableTerminalDebugLog) Console.WriteLine("[PlanningMapTest] " + message); + } + + /// 停止测试。当前测试不启动持续任务,因此无需额外清理资源。 + public override void TestStop() { } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs new file mode 100644 index 0000000..3abe48b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs @@ -0,0 +1,10 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 可选 PNG 调试图导出请求,只读取不可变的规划地图快照。 +public sealed class PlanningMapImageExportRequest +{ + /// 待渲染的规划地图快照。不能为空;PNG 导出不会改动该地图或其缓存。 + public PlanningGridMap Map { get; set; } + /// PNG 输出根目录。导出器会在其中创建 PlanningMapExports 子目录;不能为空。 + public string OutputRootDirectory { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs new file mode 100644 index 0000000..7d09fce --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs @@ -0,0 +1,20 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 一次可选 PNG 导出的处理结果,包含状态、输出路径、像素尺寸和诊断信息。 +public sealed class PlanningMapImageExportResult +{ + /// 是否已经成功写入并完成 PNG 文件。 + public bool Saved { get; set; } + /// 是否因导出开关关闭而跳过。true 时不创建文件,也不影响地图构建或缓存。 + public bool Skipped { get; set; } + /// 成功保存时的 PNG 完整路径;未保存时通常为 null。 + public string FilePath { get; set; } + /// 保存、跳过或失败的诊断信息。 + public string Message { get; set; } + /// 成功 PNG 的文件字节数;未保存时为零。 + public long FileSizeBytes { get; set; } + /// 输出图像宽度,单位为像素。 + public int PixelWidth { get; set; } + /// 输出图像高度,单位为像素。 + public int PixelHeight { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs new file mode 100644 index 0000000..be32cd4 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 可选 PNG 导出器;它永远不参与地图构建、障碍物投影或缓存指纹计算。 +public static class PlanningMapImageExporter +{ + /// 每个规划栅格在输出图中占用的边长,单位为像素。 + public const int PixelsPerCell = 4; + /// 输出 PNG 单边允许的最大像素数,超过时拒绝导出。 + public const int MaximumImageEdgePixels = 4000; + /// 输出 PNG 允许的最大文件大小,单位为字节(50 MiB)。 + public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; + /// 写入 PNG 的物理分辨率元数据,单位 DPI。 + public const float OutputDpi = 300f; + private const int MaximumFilenameAttempts = 1024; + + /// + /// 在开关开启时导出规划地图 PNG。 + /// + /// 参数:enabled 为导出开关;request 包含不可变地图和输出根目录。 + /// 返回:开关关闭时返回 Skipped;成功时返回 Saved、路径、字节数与像素尺寸;输入或尺寸不合法时返回失败信息。 + /// 注意:该方法仅用于调试,绝不会影响地图创建和缓存结果。 + /// + public static PlanningMapImageExportResult ExportIfEnabled(bool enabled, PlanningMapImageExportRequest request) + { + if (!enabled) return new PlanningMapImageExportResult { Skipped = true, Message = "Planning map image export is disabled." }; + if (request == null || request.Map == null) return Rejected("Planning map image export requires a PlanningGridMap."); + if (string.IsNullOrWhiteSpace(request.OutputRootDirectory)) return Rejected("Planning map image export requires an output root directory."); + long width = (long)request.Map.Cols * PixelsPerCell, height = (long)request.Map.Rows * PixelsPerCell; + if (width <= 0 || height <= 0 || width > MaximumImageEdgePixels || height > MaximumImageEdgePixels) return Rejected("Planning map image dimensions exceed the permitted edge.", width, height); + string temporary = null; + try + { + string directory = Path.Combine(request.OutputRootDirectory, "PlanningMapExports"); + Directory.CreateDirectory(directory); + using (FileStream stream = CreateTemporaryFile(directory, out string finalPath, out temporary)) + { + byte[] rgba = PlanningMapImageRenderer.Render(request.Map, PixelsPerCell, out int pixelWidth, out int pixelHeight); + ValidatedPngWriter.Write(rgba, pixelWidth, pixelHeight, stream); + } + long length = new FileInfo(temporary).Length; + if (length > MaximumFileSizeBytes) return Rejected("Planning map image PNG exceeds 50 MiB.", width, height); + string completed = temporary.Substring(0, temporary.Length - 4); + File.Move(temporary, completed); temporary = null; + return new PlanningMapImageExportResult { Saved = true, FilePath = completed, FileSizeBytes = length, PixelWidth = (int)width, PixelHeight = (int)height, Message = "Planning map image export saved." }; + } + catch (Exception exception) { return Rejected("Planning map image export failed: " + exception.Message, width, height); } + finally { if (temporary != null && File.Exists(temporary)) File.Delete(temporary); } + } + + private static PlanningMapImageExportResult Rejected(string message, long width = 0, long height = 0) { return new PlanningMapImageExportResult { Message = message, PixelWidth = width > int.MaxValue ? int.MaxValue : (int)width, PixelHeight = height > int.MaxValue ? int.MaxValue : (int)height }; } + private static FileStream CreateTemporaryFile(string directory, out string finalPath, out string temporaryPath) + { + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); + for (int index = 0; index < MaximumFilenameAttempts; index++) + { + string suffix = index == 0 ? string.Empty : "_" + index; + finalPath = Path.Combine(directory, "PlanningMap_" + timestamp + suffix + ".png"); temporaryPath = finalPath + ".tmp"; + if (File.Exists(finalPath)) continue; + try { return new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); } + catch (IOException) { } + } + finalPath = null; temporaryPath = null; throw new IOException("Could not reserve a unique PlanningMap PNG filename."); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs new file mode 100644 index 0000000..fbdbdbf --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs @@ -0,0 +1,37 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 从只读规划地图生成简单的 RGBA 占据图字节数组。 +internal static class PlanningMapImageRenderer +{ + /// + /// 渲染规划地图为行主序 RGBA 像素。 + /// + /// 参数:map 为只读规划快照;pixelsPerCell 为每个栅格的像素边长;width、height 返回图像像素尺寸。 + /// 返回:长度为 width × height × 4 的 RGBA 字节数组;不会修改地图。 + /// + public static byte[] Render(PlanningGridMap map, int pixelsPerCell, out int width, out int height) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + width = checked(map.Cols * pixelsPerCell); + height = checked(map.Rows * pixelsPerCell); + var rgba = new byte[checked(width * height * 4)]; + for (int row = 0; row < map.Rows; row++) + for (int col = 0; col < map.Cols; col++) + { + bool occupied = map.IsOccupied(row, col); + byte red = occupied ? (byte)220 : (byte)245; + byte green = occupied ? (byte)45 : (byte)245; + byte blue = occupied ? (byte)45 : (byte)245; + int displayRow = map.Rows - 1 - row; + for (int py = 0; py < pixelsPerCell; py++) + for (int px = 0; px < pixelsPerCell; px++) + { + int index = ((displayRow * pixelsPerCell + py) * width + col * pixelsPerCell + px) * 4; + rgba[index] = red; rgba[index + 1] = green; rgba[index + 2] = blue; rgba[index + 3] = 255; + } + } + return rgba; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs new file mode 100644 index 0000000..d2a6371 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using StbImageWriteSharp; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 编码 RGBA 像素并校验 PNG 文件头、必要块和全部 CRC 的内部写入器。 +internal static class ValidatedPngWriter +{ + private static readonly byte[] Signature = { 137, 80, 78, 71, 13, 10, 26, 10 }; + private static readonly byte[] PhysType = { 112, 72, 89, 115 }; + + /// + /// 将 RGBA 像素写入经过校验的 PNG 流。 + /// + /// 参数:rgba 为行主序、每像素四字节的红绿蓝透明度数组;width、height 为像素尺寸;output 为可写目标流。 + /// 返回:无。输入长度不等于 width × height × 4 或 PNG 校验失败时抛出异常。 + /// + public static void Write(byte[] rgba, int width, int height, Stream output) + { + Write(rgba, width, height, output, 11811u); + } + + /// 将 RGBA 像素写入带指定物理分辨率元数据的经过校验的 PNG 流。 + internal static void Write(byte[] rgba, int width, int height, Stream output, uint pixelsPerMeter) + { + if (rgba == null || output == null || width <= 0 || height <= 0 || pixelsPerMeter == 0u || rgba.Length != checked(width * height * 4)) + throw new ArgumentException("Invalid RGBA PNG input."); + byte[] encoded; + using (var memory = new MemoryStream()) + { + new ImageWriter().WritePng(rgba, width, height, ColorComponents.RedGreenBlueAlpha, memory); + encoded = memory.ToArray(); + } + Validate(encoded); + const int ihdrEndOffset = 8 + 4 + 4 + 13 + 4; + using (var outputMemory = new MemoryStream()) + { + outputMemory.Write(encoded, 0, ihdrEndOffset); + var phys = new byte[9]; + WriteUInt32BigEndian(phys, 0, pixelsPerMeter); + WriteUInt32BigEndian(phys, 4, pixelsPerMeter); + phys[8] = 1; + WriteChunk(outputMemory, PhysType, phys); + outputMemory.Write(encoded, ihdrEndOffset, encoded.Length - ihdrEndOffset); + encoded = outputMemory.ToArray(); + } + Validate(encoded); + output.Write(encoded, 0, encoded.Length); + } + + private static void Validate(byte[] png) + { + if (png == null || png.Length < 45) throw new InvalidDataException("PNG is truncated."); + for (int i = 0; i < Signature.Length; i++) if (png[i] != Signature[i]) throw new InvalidDataException("PNG signature is invalid."); + int offset = Signature.Length, ihdr = 0, iend = 0; + while (offset < png.Length) + { + if (png.Length - offset < 12) throw new InvalidDataException("PNG chunk header is truncated."); + uint length = ReadUInt32BigEndian(png, offset); + long crcOffset = (long)offset + 8L + length; + if (crcOffset + 4L > png.Length) throw new InvalidDataException("PNG chunk is truncated."); + int typeOffset = offset + 4; + uint expected = ReadUInt32BigEndian(png, (int)crcOffset); + uint actual = ComputeCrc32(png, typeOffset, checked((int)length + 4)); + if (expected != actual) throw new InvalidDataException("PNG chunk CRC is invalid."); + bool isIhdr = IsType(png, typeOffset, 73, 72, 68, 82); + bool isIend = IsType(png, typeOffset, 73, 69, 78, 68); + if (offset == Signature.Length && !isIhdr) throw new InvalidDataException("PNG must start with IHDR."); + if (isIhdr) ihdr++; + if (isIend) { iend++; if (crcOffset + 4L != png.Length) throw new InvalidDataException("PNG data follows IEND."); } + offset = checked((int)crcOffset + 4); + } + if (ihdr != 1 || iend != 1) throw new InvalidDataException("PNG must contain exactly one IHDR and IEND."); + } + private static bool IsType(byte[] bytes, int offset, byte a, byte b, byte c, byte d) { return bytes[offset] == a && bytes[offset + 1] == b && bytes[offset + 2] == c && bytes[offset + 3] == d; } + private static uint ReadUInt32BigEndian(byte[] bytes, int offset) { return ((uint)bytes[offset] << 24) | ((uint)bytes[offset + 1] << 16) | ((uint)bytes[offset + 2] << 8) | bytes[offset + 3]; } + private static void WriteUInt32BigEndian(byte[] bytes, int offset, uint value) { bytes[offset] = (byte)(value >> 24); bytes[offset + 1] = (byte)(value >> 16); bytes[offset + 2] = (byte)(value >> 8); bytes[offset + 3] = (byte)value; } + private static void WriteChunk(Stream output, byte[] type, byte[] data) + { + var length = new byte[4]; WriteUInt32BigEndian(length, 0, (uint)data.Length); output.Write(length, 0, 4); output.Write(type, 0, 4); output.Write(data, 0, data.Length); + var crcBytes = new byte[4]; WriteUInt32BigEndian(crcBytes, 0, ComputeCrc32(type, 0, type.Length, data)); output.Write(crcBytes, 0, 4); + } + private static uint ComputeCrc32(byte[] data, int offset, int count) { return ComputeCrc32(data, offset, count, null); } + private static uint ComputeCrc32(byte[] first, int offset, int count, byte[] second) + { + uint crc = 0xffffffffu; + for (int i = 0; i < count; i++) crc = UpdateCrc(crc, first[offset + i]); + if (second != null) for (int i = 0; i < second.Length; i++) crc = UpdateCrc(crc, second[i]); + return crc ^ 0xffffffffu; + } + private static uint UpdateCrc(uint crc, byte value) + { + crc ^= value; + for (int bit = 0; bit < 8; bit++) crc = (crc & 1u) == 0u ? crc >> 1 : 0xedb88320u ^ (crc >> 1); + return crc; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/CubicBSplineSmoother.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/LocalCubicBezierSmoother.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/PiecewiseQuinticSmoother.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingOptionsSnapshot.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs similarity index 85% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs index 52e5dc9..2e50fb6 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs @@ -15,7 +15,7 @@ public sealed class PathSmoothingComparisonEntry SmoothingTimingSummary timing, string stableGeometryDigest, string diagnostic) - : this(method, false, status, metrics, timing, stableGeometryDigest, diagnostic, Empty(), Empty()) + : this(method, false, status, metrics, timing, stableGeometryDigest, diagnostic, 0, 0d, Empty(), Empty()) { } @@ -27,6 +27,8 @@ public sealed class PathSmoothingComparisonEntry SmoothingTimingSummary timing, string stableGeometryDigest, string diagnostic, + int retryCount, + double acceptedStrength, IReadOnlyList path, IReadOnlyList segments) { @@ -37,6 +39,8 @@ public sealed class PathSmoothingComparisonEntry Timing = timing ?? new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, "未提供计时结果。"); StableGeometryDigest = stableGeometryDigest ?? string.Empty; Diagnostic = diagnostic ?? string.Empty; + RetryCount = retryCount < 0 ? 0 : retryCount; + AcceptedStrength = acceptedStrength; Path = CopyReadOnly(path); Segments = CopyReadOnly(segments); } @@ -62,6 +66,12 @@ public sealed class PathSmoothingComparisonEntry /// 面向报告和诊断的稳定说明。 public string Diagnostic { get; } + /// 规范平滑调用记录的重试次数;原始粗路径基线为零。 + public int RetryCount { get; } + + /// 规范平滑调用接受的强度;未接受候选和原始粗路径基线为零。 + public double AcceptedStrength { get; } + /// 仅供比较与报告读取的正式路径;失败时为空。 public IReadOnlyList Path { get; } @@ -82,11 +92,13 @@ public sealed class PathSmoothingComparisonEntry SmoothingTimingSummary timing, string stableGeometryDigest, string diagnostic, + int retryCount, + double acceptedStrength, IReadOnlyList path, IReadOnlyList segments) { return new PathSmoothingComparisonEntry( - method, false, status, metrics, timing, stableGeometryDigest, diagnostic, path, segments); + method, false, status, metrics, timing, stableGeometryDigest, diagnostic, retryCount, acceptedStrength, path, segments); } internal static PathSmoothingComparisonEntry CreateRawPathBaseline( @@ -100,7 +112,7 @@ public sealed class PathSmoothingComparisonEntry return new PathSmoothingComparisonEntry( null, true, status, metrics, new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, true, string.Empty), - stableGeometryDigest, diagnostic, path, segments); + stableGeometryDigest, diagnostic, 0, 0d, path, segments); } private static IReadOnlyList Empty() diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs new file mode 100644 index 0000000..7bc1e64 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs @@ -0,0 +1,15 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 局部 G2 五次过渡的可配置阈值。 +public sealed class LocalG2QuinticOptions +{ + public double MinimumWindowLengthMeters { get; set; } = 0.20d; + public double PreferredWindowLengthMeters { get; set; } = 0.50d; + public double MaximumWindowLengthMeters { get; set; } = 0.80d; + public double MaximumDeviationMeters { get; set; } = 0.10d; + public double AbsoluteCurvatureJumpFloorPerMeter { get; set; } = 0.001d; + public double CurvatureJumpRatioOfMaximum { get; set; } = 0.05d; + public double MinimumPeakGradientImprovementRatio { get; set; } = 0.20d; + public double MaximumVariationCostRegressionRatio { get; set; } = 0.02d; + public int MaximumCandidatesPerRegion { get; set; } = 12; +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs new file mode 100644 index 0000000..251b387 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs @@ -0,0 +1,108 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 一条原始或平滑路径的不可变质量指标。 +public sealed class PathQualityMetrics +{ + /// 创建全零、不可行的质量指标。 + public PathQualityMetrics() + : this(false, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d) + { + } + + /// 创建完整的质量指标快照。 + public PathQualityMetrics( + bool isFeasible, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters, + double lengthChangePercent, + double peakCurvatureChangePercent, + double curvatureVariationChangePercent, + double minimumClearanceChangeMeters) + : this( + isFeasible, + pathLengthMeters, + maximumAbsoluteVehicleCurvaturePerMeter, + 0d, + rootMeanSquareVehicleCurvaturePerMeter, + totalAbsoluteCurvatureVariationPerMeter, + curvatureVariationEnergy, + minimumBodyClearanceMeters, + lengthChangePercent, + peakCurvatureChangePercent, + curvatureVariationChangePercent, + minimumClearanceChangeMeters) + { + } + + /// 创建带有曲率导数峰值的完整质量指标快照。 + public PathQualityMetrics( + bool isFeasible, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters, + double lengthChangePercent, + double peakCurvatureChangePercent, + double curvatureVariationChangePercent, + double minimumClearanceChangeMeters) + { + IsFeasible = isFeasible; + PathLengthMeters = pathLengthMeters; + MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; + MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter; + RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; + TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; + CurvatureVariationEnergy = curvatureVariationEnergy; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + LengthChangePercent = lengthChangePercent; + PeakCurvatureChangePercent = peakCurvatureChangePercent; + CurvatureVariationChangePercent = curvatureVariationChangePercent; + MinimumClearanceChangeMeters = minimumClearanceChangeMeters; + } + + /// 该路径是否通过完整安全和运动学复核。 + public bool IsFeasible { get; } + + /// 路径总弧长,单位 m。 + public double PathLengthMeters { get; } + + /// 绝对车辆曲率峰值,单位 1/m。 + public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } + + /// 绝对车辆曲率导数峰值,单位 1/m²。 + public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; } + + /// 车辆曲率均方根,单位 1/m。 + public double RootMeanSquareVehicleCurvaturePerMeter { get; } + + /// 逐方向段累加的绝对曲率变化,单位 1/m。 + public double TotalAbsoluteCurvatureVariationPerMeter { get; } + + /// 逐方向段计算的曲率变化能量。 + public double CurvatureVariationEnergy { get; } + + /// 曲率变化代价的兼容名称。 + public double CurvatureVariationCost => CurvatureVariationEnergy; + + /// 完整扩大车体的最小保守净空,单位 m。 + public double MinimumBodyClearanceMeters { get; } + + /// 相对原始粗路径的长度变化百分比。 + public double LengthChangePercent { get; } + + /// 相对原始粗路径的峰值曲率变化百分比。 + public double PeakCurvatureChangePercent { get; } + + /// 相对原始粗路径的曲率变化百分比。 + public double CurvatureVariationChangePercent { get; } + + /// 相对原始粗路径的最小净空变化,单位 m。 + public double MinimumClearanceChangeMeters { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs new file mode 100644 index 0000000..013980b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的公共配置;所有距离使用 m。 +public sealed class PathSmoothingConfiguration +{ + /// 创建带有安全默认值的平滑配置。 + public PathSmoothingConfiguration() + { + OutputSpacingMeters = 0.025d; + MaximumCollisionCheckStepMeters = 0.025d; + MinimumClearanceReserveMeters = 0.02d; + SmoothingStrength = 1d; + AllowFallbackToCoarsePath = true; + RetryStrengthScales = new ReadOnlyCollection( + new List { 1d, 0.75d, 0.50d, 0.25d }); + } + + /// 正式单算法入口使用的方法。 + public SmoothingMethod Method { get; set; } + + /// 输出路径的目标弧长采样间距,单位 m。 + public double OutputSpacingMeters { get; set; } + + /// 扫掠碰撞检查的最大步长,单位 m。 + public double MaximumCollisionCheckStepMeters { get; set; } + + /// 平滑候选必须在最小净空之外保留的额外余量,单位 m。 + public double MinimumClearanceReserveMeters { get; set; } + + /// 算法初始平滑强度。 + public double SmoothingStrength { get; set; } + + /// 所有平滑尝试失败时是否允许发布经过复核的原粗路径。 + public bool AllowFallbackToCoarsePath { get; set; } + + /// 有限且严格递减的平滑强度重试比例。 + public IReadOnlyList RetryStrengthScales { get; } + + /// 三次 B 样条专用参数。 + public CubicBSplineOptions CubicBSpline { get; } = new CubicBSplineOptions(); + + /// 局部三次 Bézier 专用参数。 + public LocalCubicBezierOptions LocalCubicBezier { get; } = new LocalCubicBezierOptions(); + + /// 分段五次多项式专用参数。 + public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); + + /// 局部 G2 五次过渡专用参数。 + public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions(); +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs new file mode 100644 index 0000000..7f49d76 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs @@ -0,0 +1,80 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 一次平滑尝试的不可变诊断信息。 +public sealed class PathSmoothingDiagnostics +{ + /// 创建不含路径指标的默认诊断信息。 + public PathSmoothingDiagnostics() + : this(new PathQualityMetrics(), TimeSpan.Zero, 0, 0d, string.Empty) + { + } + + /// 创建完整的平滑诊断快照。 + public PathSmoothingDiagnostics( + PathQualityMetrics metrics, + TimeSpan elapsed, + int retryCount, + double acceptedStrength, + string terminationReason = null) + { + Metrics = metrics ?? new PathQualityMetrics(); + Elapsed = elapsed; + RetryCount = retryCount; + AcceptedStrength = acceptedStrength; + TerminationReason = terminationReason ?? string.Empty; + } + + /// 使用所有测量量创建平滑诊断快照。 + public PathSmoothingDiagnostics( + bool isFeasible, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters, + double lengthChangePercent, + double peakCurvatureChangePercent, + double curvatureVariationChangePercent, + double minimumClearanceChangeMeters, + TimeSpan elapsed, + int retryCount, + double acceptedStrength, + string terminationReason = null) + : this( + new PathQualityMetrics( + isFeasible, + pathLengthMeters, + maximumAbsoluteVehicleCurvaturePerMeter, + rootMeanSquareVehicleCurvaturePerMeter, + totalAbsoluteCurvatureVariationPerMeter, + curvatureVariationEnergy, + minimumBodyClearanceMeters, + lengthChangePercent, + peakCurvatureChangePercent, + curvatureVariationChangePercent, + minimumClearanceChangeMeters), + elapsed, + retryCount, + acceptedStrength, + terminationReason) + { + } + + /// 路径质量指标;始终非空。 + public PathQualityMetrics Metrics { get; } + + /// 从算法入口到返回诊断的耗时。 + public TimeSpan Elapsed { get; } + + /// 已执行的安全强度重试次数。 + public int RetryCount { get; } + + /// 通过复核的平滑强度;未接受候选时为零。 + public double AcceptedStrength { get; } + + /// 面向调用方的稳定终止说明。 + public string TerminationReason { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs new file mode 100644 index 0000000..91b5bf2 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs @@ -0,0 +1,17 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 局部 G2 区域未替换原始路径的稳定原因。 +public enum PathSmoothingRegionFailureReason +{ + None, + WindowUnavailable, + CandidateGenerationFailed, + Collision, + InsufficientClearance, + CurvatureExceeded, + CurvatureOvershoot, + DeviationExceeded, + InsufficientImprovement, + VariationCostRegression, + GlobalValidationRollback, +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs new file mode 100644 index 0000000..4004314 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 单个局部 G2 平滑区域的不可变发布报告。 +public sealed class PathSmoothingRegionReport +{ + public PathSmoothingRegionReport( + int segmentIndex, + double startArcLengthMeters, + double endArcLengthMeters, + IReadOnlyList curvatureJumpsPerMeter, + double plannedWindowLengthMeters, + double actualWindowLengthMeters, + double leftWindowLengthMeters, + double rightWindowLengthMeters, + int candidateCount, + int selectedCandidateIndex, + PathSmoothingRegionStatus status, + PathSmoothingRegionFailureReason failureReason, + double rawPeakCurvatureDerivativePerSquareMeter, + double resultPeakCurvatureDerivativePerSquareMeter, + double rawCurvatureVariationCost, + double resultCurvatureVariationCost, + double maximumDeviationMeters, + double minimumBodyClearanceMeters, + double maximumAbsoluteVehicleCurvaturePerMeter) + { + SegmentIndex = segmentIndex; + StartArcLengthMeters = startArcLengthMeters; + EndArcLengthMeters = endArcLengthMeters; + CurvatureJumpsPerMeter = CopyReadOnly(curvatureJumpsPerMeter); + PlannedWindowLengthMeters = plannedWindowLengthMeters; + ActualWindowLengthMeters = actualWindowLengthMeters; + LeftWindowLengthMeters = leftWindowLengthMeters; + RightWindowLengthMeters = rightWindowLengthMeters; + CandidateCount = candidateCount; + SelectedCandidateIndex = status == PathSmoothingRegionStatus.Improved + ? selectedCandidateIndex + : -1; + Status = status; + FailureReason = failureReason; + RawPeakCurvatureDerivativePerSquareMeter = rawPeakCurvatureDerivativePerSquareMeter; + ResultPeakCurvatureDerivativePerSquareMeter = resultPeakCurvatureDerivativePerSquareMeter; + RawCurvatureVariationCost = rawCurvatureVariationCost; + ResultCurvatureVariationCost = resultCurvatureVariationCost; + MaximumDeviationMeters = maximumDeviationMeters; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; + } + + public int SegmentIndex { get; } + public double StartArcLengthMeters { get; } + public double EndArcLengthMeters { get; } + public IReadOnlyList CurvatureJumpsPerMeter { get; } + public double PlannedWindowLengthMeters { get; } + public double ActualWindowLengthMeters { get; } + public double LeftWindowLengthMeters { get; } + public double RightWindowLengthMeters { get; } + public int CandidateCount { get; } + public int SelectedCandidateIndex { get; } + public PathSmoothingRegionStatus Status { get; } + public PathSmoothingRegionFailureReason FailureReason { get; } + public double RawPeakCurvatureDerivativePerSquareMeter { get; } + public double ResultPeakCurvatureDerivativePerSquareMeter { get; } + public double RawCurvatureVariationCost { get; } + public double ResultCurvatureVariationCost { get; } + public double MaximumDeviationMeters { get; } + public double MinimumBodyClearanceMeters { get; } + public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs new file mode 100644 index 0000000..752d3ef --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs @@ -0,0 +1,8 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 单个局部 G2 平滑区域的处理结果。 +public enum PathSmoothingRegionStatus +{ + Improved, + RetainedOriginal, +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs new file mode 100644 index 0000000..ebe8106 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑所需的原始粗路径、复核上下文和配置。 +public sealed class PathSmoothingRequest +{ + private readonly VehicleParameters _vehicle; + private readonly PathSmoothingConfiguration _configuration; + + /// 创建路径平滑请求,并复制粗路径和方向分段集合。 + public PathSmoothingRequest( + IReadOnlyList coarsePath, + IReadOnlyList segments, + PlanningGridMap map, + VehicleParameters vehicle, + PathSmoothingConfiguration configuration) + { + CoarsePath = CopyReadOnly(coarsePath); + Segments = CopyReadOnly(segments); + Map = map; + _vehicle = CopyVehicle(vehicle); + _configuration = CopyConfiguration(configuration); + } + + /// 原始粗路径的不可变快照。 + public IReadOnlyList CoarsePath { get; } + + /// 原始粗路径方向分段的不可变快照。 + public IReadOnlyList Segments { get; } + + /// 用于平滑后完整车体复核的规划栅格地图。 + public PlanningGridMap Map { get; } + + /// 车辆几何与最大曲率约束的不可变快照副本。 + public VehicleParameters Vehicle => CopyVehicle(_vehicle); + + /// 本次平滑配置的不可变快照副本。 + public PathSmoothingConfiguration Configuration => CopyConfiguration(_configuration); + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } + + private static VehicleParameters CopyVehicle(VehicleParameters source) + { + if (source == null) return null; + return new VehicleParameters + { + LengthMeters = source.LengthMeters, + WidthMeters = source.WidthMeters, + SafetyMarginMeters = source.SafetyMarginMeters, + MaximumCurvaturePerMeter = source.MaximumCurvaturePerMeter, + MinimumTurningRadiusMeters = source.MinimumTurningRadiusMeters, + }; + } + + private static PathSmoothingConfiguration CopyConfiguration(PathSmoothingConfiguration source) + { + if (source == null) return null; + var copy = new PathSmoothingConfiguration + { + Method = source.Method, + OutputSpacingMeters = source.OutputSpacingMeters, + MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters, + MinimumClearanceReserveMeters = source.MinimumClearanceReserveMeters, + SmoothingStrength = source.SmoothingStrength, + AllowFallbackToCoarsePath = source.AllowFallbackToCoarsePath, + }; + copy.CubicBSpline.EndpointTangentScale = source.CubicBSpline.EndpointTangentScale; + copy.LocalCubicBezier.CornerHeadingThresholdRadians = source.LocalCubicBezier.CornerHeadingThresholdRadians; + copy.LocalCubicBezier.MaximumWindowLengthMeters = source.LocalCubicBezier.MaximumWindowLengthMeters; + copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio; + copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters; + copy.PiecewiseQuintic.MinimumKnotSpacingMeters = source.PiecewiseQuintic.MinimumKnotSpacingMeters; + copy.LocalG2Quintic.MinimumWindowLengthMeters = source.LocalG2Quintic.MinimumWindowLengthMeters; + copy.LocalG2Quintic.PreferredWindowLengthMeters = source.LocalG2Quintic.PreferredWindowLengthMeters; + copy.LocalG2Quintic.MaximumWindowLengthMeters = source.LocalG2Quintic.MaximumWindowLengthMeters; + copy.LocalG2Quintic.MaximumDeviationMeters = source.LocalG2Quintic.MaximumDeviationMeters; + copy.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = source.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter; + copy.LocalG2Quintic.CurvatureJumpRatioOfMaximum = source.LocalG2Quintic.CurvatureJumpRatioOfMaximum; + copy.LocalG2Quintic.MinimumPeakGradientImprovementRatio = source.LocalG2Quintic.MinimumPeakGradientImprovementRatio; + copy.LocalG2Quintic.MaximumVariationCostRegressionRatio = source.LocalG2Quintic.MaximumVariationCostRegressionRatio; + copy.LocalG2Quintic.MaximumCandidatesPerRegion = source.LocalG2Quintic.MaximumCandidatesPerRegion; + return copy; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs new file mode 100644 index 0000000..d964caa --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的最终不可变结果。 +public sealed class PathSmoothingResult +{ + private static readonly IReadOnlyList EmptyPath = + new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptySegments = + new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptyRegionReports = + new ReadOnlyCollection(new List()); + + private PathSmoothingResult( + PathSmoothingStatus status, + SmoothingMethod? method, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics, + IReadOnlyList regionReports) + { + Status = status; + Method = method; + Path = path; + Segments = segments; + RegionReports = regionReports; + Diagnostics = diagnostics ?? new PathSmoothingDiagnostics( + new PathQualityMetrics(), + TimeSpan.Zero, + 0, + 0d, + "No smoothing diagnostics were supplied."); + } + + /// 最终发布状态。 + public PathSmoothingStatus Status { get; } + + /// 成功或回退时的实际(或尝试)平滑方法;失败时为空。 + public SmoothingMethod? Method { get; } + + /// 成功或经过复核的回退路径;其他状态始终为空且不可变。 + public IReadOnlyList Path { get; } + + /// 覆盖 的方向分段;其他状态始终为空且不可变。 + public IReadOnlyList Segments { get; } + + /// 局部 G2 各检测区域的不可变报告;传统算法结果为空。 + public IReadOnlyList RegionReports { get; } + + /// 本次平滑的质量和终止诊断;始终非空。 + public PathSmoothingDiagnostics Diagnostics { get; } + + /// 创建已通过所有复核的平滑结果。 + public static PathSmoothingResult Success( + SmoothingMethod method, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + ValidatePublishedResult(method, path, segments, diagnostics); + return new PathSmoothingResult( + PathSmoothingStatus.Success, + method, + CopyReadOnly(path), + CopyReadOnly(segments), + diagnostics, + EmptyRegionReports); + } + + /// 创建经过完整复核的原始粗路径回退结果。 + public static PathSmoothingResult Fallback( + SmoothingMethod attemptedMethod, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + ValidatePublishedResult(attemptedMethod, path, segments, diagnostics); + return new PathSmoothingResult( + PathSmoothingStatus.FallbackToCoarsePath, + attemptedMethod, + CopyReadOnly(path), + CopyReadOnly(segments), + diagnostics, + EmptyRegionReports); + } + + /// 发布经过完整复核的局部 G2 预平滑结果。 + public static PathSmoothingResult PublishLocalG2( + PathSmoothingStatus status, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics, + IReadOnlyList regionReports) + { + if (status != PathSmoothingStatus.Complete && + status != PathSmoothingStatus.PartialImprovement && + status != PathSmoothingStatus.NotNeeded && + status != PathSmoothingStatus.Unchanged) + throw new ArgumentException("Use a Local G2 publication status.", nameof(status)); + if (regionReports == null) + throw new ArgumentNullException(nameof(regionReports)); + + ValidatePublishedResult(SmoothingMethod.LocalG2Quintic, path, segments, diagnostics); + return new PathSmoothingResult( + status, + SmoothingMethod.LocalG2Quintic, + CopyReadOnly(path), + CopyReadOnly(segments), + diagnostics, + CopyReadOnly(regionReports)); + } + + /// 创建不发布路径的失败、不可行、取消或输入无效结果。 + public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics) + { + if (status == PathSmoothingStatus.Success || + status == PathSmoothingStatus.FallbackToCoarsePath || + status == PathSmoothingStatus.Complete || + status == PathSmoothingStatus.PartialImprovement || + status == PathSmoothingStatus.NotNeeded || + status == PathSmoothingStatus.Unchanged) + throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status)); + if (!Enum.IsDefined(typeof(PathSmoothingStatus), status)) + throw new ArgumentOutOfRangeException(nameof(status)); + return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics, EmptyRegionReports); + } + + private static void ValidatePublishedResult( + SmoothingMethod method, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + if (!Enum.IsDefined(typeof(SmoothingMethod), method)) + throw new ArgumentOutOfRangeException(nameof(method)); + if (path == null || path.Count == 0) + throw new ArgumentException("Published smoothing results require a non-empty path.", nameof(path)); + if (segments == null || segments.Count == 0) + throw new ArgumentException("Published smoothing results require non-empty segments.", nameof(segments)); + if (diagnostics == null || diagnostics.Metrics == null || !diagnostics.Metrics.IsFeasible) + throw new ArgumentException("Published smoothing results require feasible diagnostics.", nameof(diagnostics)); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs new file mode 100644 index 0000000..df1cdab --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs @@ -0,0 +1,16 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的最终发布状态。 +public enum PathSmoothingStatus +{ + Success, + FallbackToCoarsePath, + InvalidInput, + Infeasible, + Failed, + Cancelled, + Complete, + PartialImprovement, + NotNeeded, + Unchanged, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs similarity index 100% rename from ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs rename to .task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs new file mode 100644 index 0000000..b0a3c8a --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs @@ -0,0 +1,100 @@ +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑空间路径上的不可变采样点;位置和长度单位为 m,航向为 rad,曲率为 1/m。 +public sealed class SmoothedPathPoint +{ + public SmoothedPathPoint( + double xMeters, + double yMeters, + double headingRadians, + double unwrappedHeadingRadians, + double arcLengthMeters, + TravelDirection direction, + double geometricCurvaturePerMeter, + double vehicleCurvaturePerMeter, + double bodyClearanceMeters, + bool isGearSwitchPoint, + SmoothedPathPointSource source) + : this( + xMeters, + yMeters, + headingRadians, + unwrappedHeadingRadians, + arcLengthMeters, + direction, + geometricCurvaturePerMeter, + vehicleCurvaturePerMeter, + 0d, + bodyClearanceMeters, + isGearSwitchPoint, + source) + { + } + + /// 创建带有车辆曲率对弧长导数的不可变采样点。 + public SmoothedPathPoint( + double xMeters, + double yMeters, + double headingRadians, + double unwrappedHeadingRadians, + double arcLengthMeters, + TravelDirection direction, + double geometricCurvaturePerMeter, + double vehicleCurvaturePerMeter, + double vehicleCurvatureDerivativePerSquareMeter, + double bodyClearanceMeters, + bool isGearSwitchPoint, + SmoothedPathPointSource source) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + UnwrappedHeading = unwrappedHeadingRadians; + ArcLength = arcLengthMeters; + Direction = direction; + GeometricCurvature = geometricCurvaturePerMeter; + VehicleCurvature = vehicleCurvaturePerMeter; + VehicleCurvatureDerivative = vehicleCurvatureDerivativePerSquareMeter; + BodyClearance = bodyClearanceMeters; + IsGearSwitchPoint = isGearSwitchPoint; + Source = source; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 归一化的车辆航向,单位 rad。 + public double Heading { get; } + + /// 连续展开的车辆航向,单位 rad。 + public double UnwrappedHeading { get; } + + /// 从完整路径起点累计的弧长,单位 m。 + public double ArcLength { get; } + + /// 该点所在连续方向段的行驶方向。 + public TravelDirection Direction { get; } + + /// 按几何弧长计算的有符号路径曲率,单位 1/m。 + public double GeometricCurvature { get; } + + /// 车辆模型使用的有符号曲率,单位 1/m。 + public double VehicleCurvature { get; } + + /// 车辆曲率对弧长的导数 dκ/ds,单位 1/m²。 + public double VehicleCurvatureDerivative { get; } + + /// 扩大车体后的保守净空下界,单位 m。 + public double BodyClearance { get; } + + /// 该点是否为新方向段开始的换向点。 + public bool IsGearSwitchPoint { get; } + + /// 该点在平滑流程中的来源。 + public SmoothedPathPointSource Source { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs new file mode 100644 index 0000000..9469fc6 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑路径点的来源。 +public enum SmoothedPathPointSource +{ + Anchor, + Interpolated, + GearSwitch, + CoarsePathFallback, + LocalG2Transition, +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs new file mode 100644 index 0000000..5ce1d95 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs @@ -0,0 +1,41 @@ +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑路径中方向一致的连续点范围;起止索引均包含在内。 +public sealed class SmoothedPathSegment +{ + public SmoothedPathSegment( + int segmentIndex, + TravelDirection direction, + int startIndex, + int endIndex, + bool startsAtGearSwitch, + bool endsAtGearSwitch) + { + SegmentIndex = segmentIndex; + Direction = direction; + StartIndex = startIndex; + EndIndex = endIndex; + StartsAtGearSwitch = startsAtGearSwitch; + EndsAtGearSwitch = endsAtGearSwitch; + } + + /// 从零开始的分段序号。 + public int SegmentIndex { get; } + + /// 本段行驶方向。 + public TravelDirection Direction { get; } + + /// 本段在平滑路径中的起始包含式索引。 + public int StartIndex { get; } + + /// 本段在平滑路径中的结束包含式索引。 + public int EndIndex { get; } + + /// 本段首点是否为换向后保留的新方向点。 + public bool StartsAtGearSwitch { get; } + + /// 本段末点是否紧邻下一方向段的换向对。 + public bool EndsAtGearSwitch { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs new file mode 100644 index 0000000..890e02f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs @@ -0,0 +1,10 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 支持的粗路径平滑方法。 +public enum SmoothingMethod +{ + CubicBSpline, + LocalCubicBezier, + PiecewiseQuintic, + LocalG2Quintic, +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs new file mode 100644 index 0000000..91521bc --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; + +/// 以固定预热和五次测量隔离比较所有请求平滑方法的离线入口。 +public sealed class PathSmoothingComparisonService +{ + private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); + private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); + + /// 比较所有请求方法;一个方法的失败不会阻止其他方法,取消会停止后续启动。 + public PathSmoothingComparisonResult Compare( + PathSmoothingComparisonRequest request, + CancellationToken cancellationToken = default) + { + PathSmoothingComparisonEntry baseline = CreateRawPathBaseline(request, out string baselineReason); + var entries = new List(); + + if (cancellationToken.IsCancellationRequested) + return Cancelled(baseline, entries); + + if (request == null || request.SmoothingRequest == null) + return new PathSmoothingComparisonResult(baseline, entries, null, false, baselineReason); + + for (int methodIndex = 0; methodIndex < request.Methods.Count; methodIndex++) + { + if (cancellationToken.IsCancellationRequested) + return Cancelled(baseline, entries); + + SmoothingMethod method = request.Methods[methodIndex]; + if (!TryCompareMethod( + request.SmoothingRequest, + method, + baseline.Metrics, + cancellationToken, + out PathSmoothingComparisonEntry entry)) + return Cancelled(baseline, entries); + entries.Add(entry); + } + + return new PathSmoothingComparisonResult( + baseline, + entries, + SmoothingMethodRanker.Rank(entries), + false, + baselineReason); + } + + private bool TryCompareMethod( + PathSmoothingRequest sourceRequest, + SmoothingMethod method, + PathQualityMetrics rawMetrics, + CancellationToken cancellationToken, + out PathSmoothingComparisonEntry entry) + { + entry = null; + try + { + PathSmoothingRequest methodRequest = CreateMethodRequest(sourceRequest, method); + cancellationToken.ThrowIfCancellationRequested(); + PathSmoothingResult warmup = _smoothingService.Smooth(methodRequest, cancellationToken); + if (warmup.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; + + var timings = new List(5); + var measuredResults = new List(5); + for (int sampleIndex = 0; sampleIndex < 5; sampleIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + Stopwatch stopwatch = Stopwatch.StartNew(); + PathSmoothingResult result = _smoothingService.Smooth(methodRequest, cancellationToken); + stopwatch.Stop(); + if (result.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; + timings.Add(stopwatch.Elapsed.TotalMilliseconds); + measuredResults.Add(result); + } + + PathSmoothingResult canonical = measuredResults[0]; + string digest = StableGeometryDigest.Compute(canonical); + SmoothingTimingSummary timing = SmoothingTimingSummary.FromMeasurements(timings, measuredResults); + string diagnostic = string.IsNullOrWhiteSpace(timing.Diagnostic) + ? canonical.Diagnostics.TerminationReason + : timing.Diagnostic; + + PathQualityMetrics metrics = canonical.Status == PathSmoothingStatus.Success + ? NormalizeMetrics(canonical.Diagnostics.Metrics, rawMetrics) + : new PathQualityMetrics(); + entry = PathSmoothingComparisonEntry.CreateCandidate( + method, + canonical.Status, + metrics, + timing, + digest, + diagnostic, + canonical.Diagnostics.RetryCount, + canonical.Diagnostics.AcceptedStrength, + canonical.Path, + canonical.Segments); + return true; + } + catch (OperationCanceledException) + { + return false; + } + catch (Exception exception) + { + entry = PathSmoothingComparisonEntry.CreateCandidate( + method, + PathSmoothingStatus.Failed, + new PathQualityMetrics(), + new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, exception.GetType().Name), + string.Empty, + exception.Message, + 0, + 0d, + null, + null); + return true; + } + } + + private PathSmoothingComparisonEntry CreateRawPathBaseline( + PathSmoothingComparisonRequest request, + out string reason) + { + reason = string.Empty; + if (request == null || request.SmoothingRequest == null) + return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求为空。", out reason); + + PathSmoothingRequest smoothingRequest = request.SmoothingRequest; + PathSmoothingConfiguration configuration = smoothingRequest.Configuration; + if (configuration == null || smoothingRequest.Map == null || smoothingRequest.Vehicle == null) + return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求缺少可用的地图、车辆或配置。", out reason); + + if (!_preprocessor.TryPrepare(smoothingRequest, out PreparedPath preparedPath, out reason)) + return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + if (!RawPathBaselineBuilder.TryCreate( + smoothingRequest, + preparedPath, + _analyzer, + configuration.OutputSpacingMeters, + _validator, + configuration.MaximumCollisionCheckStepMeters, + out RawPathBaseline rawPath, + out reason)) + return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + + string digest = StableGeometryDigest.Compute(PathSmoothingStatus.Success, null, rawPath.Path, rawPath.Segments); + return PathSmoothingComparisonEntry.CreateRawPathBaseline( + PathSmoothingStatus.Success, + rawPath.Metrics, + digest, + string.Empty, + rawPath.Path, + rawPath.Segments); + } + + private static PathSmoothingComparisonEntry FailedBaseline( + PathSmoothingStatus status, + string failureReason, + out string reason) + { + reason = failureReason ?? string.Empty; + return PathSmoothingComparisonEntry.CreateRawPathBaseline( + status, + new PathQualityMetrics(), + StableGeometryDigest.Compute(status, null, null, null), + reason, + null, + null); + } + + private static PathSmoothingComparisonResult Cancelled( + PathSmoothingComparisonEntry baseline, + IReadOnlyList entries) + { + return new PathSmoothingComparisonResult(baseline, entries, null, true, "路径平滑比较已取消。"); + } + + private static PathSmoothingRequest CreateMethodRequest(PathSmoothingRequest source, SmoothingMethod method) + { + PathSmoothingConfiguration configuration = source.Configuration; + configuration.Method = method; + configuration.AllowFallbackToCoarsePath = false; + return new PathSmoothingRequest( + source.CoarsePath, + source.Segments, + source.Map, + source.Vehicle, + configuration); + } + + private static PathQualityMetrics NormalizeMetrics( + PathQualityMetrics candidate, + PathQualityMetrics raw) + { + if (candidate == null || raw == null || !candidate.IsFeasible) + return new PathQualityMetrics(); + + return new PathQualityMetrics( + true, + candidate.PathLengthMeters, + candidate.MaximumAbsoluteVehicleCurvaturePerMeter, + candidate.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + candidate.RootMeanSquareVehicleCurvaturePerMeter, + candidate.TotalAbsoluteCurvatureVariationPerMeter, + candidate.CurvatureVariationEnergy, + candidate.MinimumBodyClearanceMeters, + RelativePercentOrAbsoluteDelta(candidate.PathLengthMeters, raw.PathLengthMeters), + RelativePercentOrAbsoluteDelta( + candidate.MaximumAbsoluteVehicleCurvaturePerMeter, + raw.MaximumAbsoluteVehicleCurvaturePerMeter), + RelativePercentOrAbsoluteDelta( + candidate.TotalAbsoluteCurvatureVariationPerMeter, + raw.TotalAbsoluteCurvatureVariationPerMeter), + candidate.MinimumBodyClearanceMeters - raw.MinimumBodyClearanceMeters); + } + + private static double RelativePercentOrAbsoluteDelta(double candidate, double raw) + { + double delta = candidate - raw; + return Math.Abs(raw) < 1e-12d ? delta : delta / raw * 100d; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs new file mode 100644 index 0000000..5b1db56 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; + +/// 正式单算法路径平滑入口,负责输入校验、有限重试与经过复核的粗路径回退。 +public sealed class PathSmoothingService +{ + private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothingAlgorithmRunner _runner = new SmoothingAlgorithmRunner(); + private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); + private readonly IPathSmoother _bSpline = new CubicBSplineSmoother(); + private readonly IPathSmoother _bezier = new LocalCubicBezierSmoother(); + private readonly IPathSmoother _quintic = new PiecewiseQuinticSmoother(); + private readonly LocalG2PreSmoothingPipeline _localG2Pipeline = new LocalG2PreSmoothingPipeline(); + + /// 执行一次经过完整安全复核的单算法平滑。 + public PathSmoothingResult Smooth( + PathSmoothingRequest request, + CancellationToken cancellationToken = default) + { + var stopwatch = Stopwatch.StartNew(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + if (!TryValidateRequest(request, out PathSmoothingConfiguration configuration, out string reason)) + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + + if (!_preprocessor.TryPrepare(request, out PreparedPath preparedPath, out reason)) + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + + if (!RawPathBaselineBuilder.TryCreate( + request, + preparedPath, + _analyzer, + configuration.OutputSpacingMeters, + _validator, + configuration.MaximumCollisionCheckStepMeters, + out RawPathBaseline rawBaseline, + out reason)) + { + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + } + + if (configuration.Method == SmoothingMethod.LocalG2Quintic) + return _localG2Pipeline.Smooth(request, preparedPath, rawBaseline, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + var input = new SmoothingAlgorithmInput( + preparedPath, + request.Map, + request.Vehicle, + configuration.MaximumCollisionCheckStepMeters, + configuration.MinimumClearanceReserveMeters, + new SmoothingOptionsSnapshot(configuration)); + SmoothingAlgorithmRunner.AlgorithmRunResult runResult = _runner.Run( + Resolve(configuration.Method), input, configuration, cancellationToken); + int retryCount = GetRetryCount(runResult.AttemptedStrengths); + PathSmoothingDiagnostics diagnostics = new PathSmoothingDiagnostics( + runResult.Metrics, + stopwatch.Elapsed, + retryCount, + runResult.AcceptedStrength, + runResult.Reason); + + if (runResult.Status == PathSmoothingStatus.Success) + { + return PathSmoothingResult.Success( + configuration.Method, + runResult.Path, + runResult.Segments, + diagnostics); + } + + if (!configuration.AllowFallbackToCoarsePath) + return PathSmoothingResult.Failure(runResult.Status, diagnostics); + + cancellationToken.ThrowIfCancellationRequested(); + if (!TryCreateVerifiedFallback( + request, + configuration, + cancellationToken, + out IReadOnlyList fallbackPath, + out IReadOnlyList fallbackSegments, + out PathQualityMetrics fallbackMetrics, + out reason)) + { + return PathSmoothingResult.Failure(runResult.Status, diagnostics); + } + + var fallbackDiagnostics = new PathSmoothingDiagnostics( + fallbackMetrics, + stopwatch.Elapsed, + retryCount, + runResult.AcceptedStrength, + runResult.Reason); + return PathSmoothingResult.Fallback( + configuration.Method, + fallbackPath, + fallbackSegments, + fallbackDiagnostics); + } + catch (OperationCanceledException) + { + return Failure(PathSmoothingStatus.Cancelled, stopwatch, 0, 0d, "路径平滑已取消。"); + } + catch (Exception exception) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, 0, 0d, exception.Message); + } + } + + private bool TryCreateVerifiedFallback( + PathSmoothingRequest request, + PathSmoothingConfiguration configuration, + CancellationToken cancellationToken, + out IReadOnlyList fallbackPath, + out IReadOnlyList fallbackSegments, + out PathQualityMetrics fallbackMetrics, + out string reason) + { + fallbackPath = null; + fallbackSegments = null; + fallbackMetrics = null; + reason = string.Empty; + + // Reprepare from the immutable request instead of reusing the algorithm input: fallback is a + // separately published output and must repeat the coarse-path contract validation. + if (!_preprocessor.TryPrepare(request, out PreparedPath revalidatedPath, out reason)) return false; + cancellationToken.ThrowIfCancellationRequested(); + + if (!RawPathBaselineBuilder.TryCreate( + request, + revalidatedPath, + _analyzer, + configuration.OutputSpacingMeters, + _validator, + configuration.MaximumCollisionCheckStepMeters, + out RawPathBaseline rawPath, + out reason)) + { + return false; + } + + fallbackPath = ToFallbackPoints(rawPath.Path); + fallbackSegments = rawPath.Segments; + fallbackMetrics = rawPath.Metrics; + return true; + } + + private IPathSmoother Resolve(SmoothingMethod method) + { + return method switch + { + SmoothingMethod.CubicBSpline => _bSpline, + SmoothingMethod.LocalCubicBezier => _bezier, + SmoothingMethod.PiecewiseQuintic => _quintic, + _ => throw new ArgumentOutOfRangeException(nameof(method)), + }; + } + + private static bool TryValidateRequest( + PathSmoothingRequest request, + out PathSmoothingConfiguration configuration, + out string reason) + { + configuration = null; + reason = string.Empty; + if (request == null) + { + reason = "平滑请求为空。"; + return false; + } + + configuration = request.Configuration; + VehicleParameters vehicle = request.Vehicle; + if (configuration == null || request.Map == null || !request.Map.PlanningReady || vehicle == null) + { + reason = "平滑请求缺少可用的地图、车辆或配置。"; + return false; + } + + if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || + !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || + !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out _)) + { + reason = "平滑请求中的车辆几何或曲率约束无效。"; + return false; + } + + if (!Enum.IsDefined(typeof(SmoothingMethod), configuration.Method)) + { + reason = "平滑方法无效。"; + return false; + } + + if (!NumericGuard.IsPositiveFinite(configuration.OutputSpacingMeters) || + !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || + !NumericGuard.IsFinite(configuration.MinimumClearanceReserveMeters) || + configuration.MinimumClearanceReserveMeters < 0d || + !NumericGuard.IsPositiveFinite(configuration.SmoothingStrength)) + { + reason = "平滑配置包含非法数值或不满足方法契约。"; + return false; + } + + if (configuration.Method == SmoothingMethod.LocalG2Quintic) + return IsValidLocalG2Options(configuration.LocalG2Quintic, out reason); + + if ( + !NumericGuard.IsPositiveFinite(configuration.CubicBSpline.EndpointTangentScale) || + !IsValidBezierThreshold(configuration.LocalCubicBezier.CornerHeadingThresholdRadians) || + !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.MaximumWindowLengthMeters) || + !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.HandleLengthRatio) || + !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.KnotSpacingMeters) || + !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) || + configuration.PiecewiseQuintic.KnotSpacingMeters < configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) + { + reason = "平滑配置包含非法数值或不满足方法契约。"; + return false; + } + return true; + } + + private static bool IsValidLocalG2Options(LocalG2QuinticOptions options, out string reason) + { + reason = string.Empty; + if (options != null && + NumericGuard.IsPositiveFinite(options.MinimumWindowLengthMeters) && + NumericGuard.IsFinite(options.PreferredWindowLengthMeters) && + options.PreferredWindowLengthMeters >= options.MinimumWindowLengthMeters && + NumericGuard.IsFinite(options.MaximumWindowLengthMeters) && + options.MaximumWindowLengthMeters >= options.PreferredWindowLengthMeters && + NumericGuard.IsPositiveFinite(options.MaximumDeviationMeters) && + NumericGuard.IsPositiveFinite(options.AbsoluteCurvatureJumpFloorPerMeter) && + NumericGuard.IsFinite(options.CurvatureJumpRatioOfMaximum) && + options.CurvatureJumpRatioOfMaximum > 0d && options.CurvatureJumpRatioOfMaximum <= 1d && + NumericGuard.IsFinite(options.MinimumPeakGradientImprovementRatio) && + options.MinimumPeakGradientImprovementRatio > 0d && options.MinimumPeakGradientImprovementRatio < 1d && + NumericGuard.IsFinite(options.MaximumVariationCostRegressionRatio) && + options.MaximumVariationCostRegressionRatio >= 0d && options.MaximumCandidatesPerRegion >= 1) + { + return true; + } + + reason = "局部 G2 配置包含非法数值或不满足方法契约。"; + return false; + } + + private static bool IsValidBezierThreshold(double thresholdRadians) + { + return NumericGuard.IsFinite(thresholdRadians) && thresholdRadians > 0d && thresholdRadians <= Math.PI; + } + + private static IReadOnlyList ToFallbackPoints(IReadOnlyList path) + { + var points = new List(path.Count); + for (int index = 0; index < path.Count; index++) + { + SmoothedPathPoint point = path[index]; + points.Add(new SmoothedPathPoint( + point.X, + point.Y, + point.Heading, + point.UnwrappedHeading, + point.ArcLength, + point.Direction, + point.GeometricCurvature, + point.VehicleCurvature, + point.VehicleCurvatureDerivative, + point.BodyClearance, + point.IsGearSwitchPoint, + SmoothedPathPointSource.CoarsePathFallback)); + } + return points; + } + + private static int GetRetryCount(IReadOnlyList attemptedStrengths) + { + return attemptedStrengths == null || attemptedStrengths.Count == 0 ? 0 : attemptedStrengths.Count - 1; + } + + private static PathSmoothingResult Failure( + PathSmoothingStatus status, + Stopwatch stopwatch, + int retryCount, + double acceptedStrength, + string reason) + { + return PathSmoothingResult.Failure( + status, + new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, retryCount, acceptedStrength, reason)); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs new file mode 100644 index 0000000..671f3ca --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransition.cs @@ -0,0 +1,55 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 同一方向原语边界两侧车辆曲率的离散跳变。 +internal sealed class CurvatureTransition +{ + internal CurvatureTransition( + int segmentIndex, + int leftCoarsePathIndex, + int rightCoarsePathIndex, + double localArcLengthMeters, + double x, + double y, + double vehicleHeadingRadians, + double leftVehicleCurvaturePerMeter, + double rightVehicleCurvaturePerMeter) + { + if (segmentIndex < 0 || leftCoarsePathIndex < 0 || rightCoarsePathIndex != leftCoarsePathIndex + 1) + throw new ArgumentOutOfRangeException(nameof(leftCoarsePathIndex)); + if (!NumericGuard.IsFinite(localArcLengthMeters) || localArcLengthMeters < 0d || + !NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(vehicleHeadingRadians) || + !NumericGuard.IsFinite(leftVehicleCurvaturePerMeter) || !NumericGuard.IsFinite(rightVehicleCurvaturePerMeter)) + { + throw new ArgumentOutOfRangeException(nameof(localArcLengthMeters)); + } + + double curvatureJumpPerMeter = Math.Abs(rightVehicleCurvaturePerMeter - leftVehicleCurvaturePerMeter); + if (!NumericGuard.IsFinite(curvatureJumpPerMeter)) + throw new ArgumentOutOfRangeException(nameof(rightVehicleCurvaturePerMeter)); + + SegmentIndex = segmentIndex; + LeftCoarsePathIndex = leftCoarsePathIndex; + RightCoarsePathIndex = rightCoarsePathIndex; + LocalArcLengthMeters = localArcLengthMeters; + X = x; + Y = y; + VehicleHeadingRadians = vehicleHeadingRadians; + LeftVehicleCurvaturePerMeter = leftVehicleCurvaturePerMeter; + RightVehicleCurvaturePerMeter = rightVehicleCurvaturePerMeter; + CurvatureJumpPerMeter = curvatureJumpPerMeter; + } + + internal int SegmentIndex { get; } + internal int LeftCoarsePathIndex { get; } + internal int RightCoarsePathIndex { get; } + internal double LocalArcLengthMeters { get; } + internal double X { get; } + internal double Y { get; } + internal double VehicleHeadingRadians { get; } + internal double LeftVehicleCurvaturePerMeter { get; } + internal double RightVehicleCurvaturePerMeter { get; } + internal double CurvatureJumpPerMeter { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs new file mode 100644 index 0000000..54d95ba --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/CurvatureTransitionDetector.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 从原始粗路径的同向相邻点中识别恒曲率原语边界。 +internal sealed class CurvatureTransitionDetector +{ + internal bool TryDetect( + PathSmoothingRequest request, + double maximumVehicleCurvaturePerMeter, + LocalG2OptionsSnapshot options, + out IReadOnlyList transitions, + out string reason) + { + transitions = Empty(); + reason = string.Empty; + if (request == null || request.CoarsePath == null || request.Segments == null || options == null || + !NumericGuard.IsPositiveFinite(maximumVehicleCurvaturePerMeter)) + { + reason = "局部 G2 曲率事件检测输入无效。"; + return false; + } + if (!ValidatePath(request.CoarsePath, out reason) || !ValidateSegments(request.CoarsePath, request.Segments, out reason)) + return false; + + double threshold = Math.Max( + options.AbsoluteCurvatureJumpFloorPerMeter, + options.CurvatureJumpRatioOfMaximum * maximumVehicleCurvaturePerMeter); + if (!NumericGuard.IsPositiveFinite(threshold)) + { + reason = "局部 G2 曲率事件阈值无效。"; + return false; + } + + var detected = new List(); + for (int segmentPosition = 0; segmentPosition < request.Segments.Count; segmentPosition++) + { + PathSegment segment = request.Segments[segmentPosition]; + double segmentStartArc = request.CoarsePath[segment.StartIndex].ArcLength; + for (int leftIndex = segment.StartIndex; leftIndex < segment.EndIndex; leftIndex++) + { + CoarsePathPoint left = request.CoarsePath[leftIndex]; + CoarsePathPoint right = request.CoarsePath[leftIndex + 1]; + double delta = right.VehicleCurvature - left.VehicleCurvature; + if (!NumericGuard.IsFinite(delta)) + { + reason = "局部 G2 曲率事件跳变溢出。"; + return false; + } + if (Math.Abs(delta) >= threshold) + { + detected.Add(new CurvatureTransition( + segment.SegmentIndex, + leftIndex, + leftIndex + 1, + left.ArcLength - segmentStartArc, + left.X, + left.Y, + left.Heading, + left.VehicleCurvature, + right.VehicleCurvature)); + } + } + } + transitions = new ReadOnlyCollection(detected); + return true; + } + + /// 反射脚本使用的确定性检测与窗口规划接缝。 + public static class TestHooks + { + public static DetectionTestSnapshot Execute(string scenario) + { + if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario)); + switch (scenario) + { + case "SingleTransition": return DetectSingleTransition(); + case "GearSwitch": return DetectGearSwitch(); + case "Noise": return DetectNoise(); + case "Overlap": return PlanOverlap(); + case "NearStart": return PlanNearStart(); + default: throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + + public sealed class DetectionTestSnapshot + { + internal DetectionTestSnapshot(int transitionCount, double maximumJump, int regionCount, + int transitionCountInFirstRegion, double startArcLength, double leftWindowLength, double rightWindowLength) + { + TransitionCount = transitionCount; + MaximumJump = maximumJump; + RegionCount = regionCount; + TransitionCountInFirstRegion = transitionCountInFirstRegion; + StartArcLength = startArcLength; + LeftWindowLength = leftWindowLength; + RightWindowLength = rightWindowLength; + } + + public int TransitionCount { get; } + public double MaximumJump { get; } + public int RegionCount { get; } + public int TransitionCountInFirstRegion { get; } + public double StartArcLength { get; } + public double LeftWindowLength { get; } + public double RightWindowLength { get; } + } + + private static DetectionTestSnapshot DetectSingleTransition() + { + IReadOnlyList transitions = Detect(CreateRequest( + new[] { Point(0d, 0d), Point(0.1d, 0.4167d), Point(0.2d, 0.4167d) }, + new[] { new PathSegment(0, TravelDirection.Forward, 0, 2, false, false) })); + return Snapshot(transitions); + } + + private static DetectionTestSnapshot DetectGearSwitch() + { + IReadOnlyList transitions = Detect(CreateRequest( + new[] + { + Point(0d, 0d, TravelDirection.Forward), Point(0.2d, 0d, TravelDirection.Forward), + Point(0.2d, 0.4167d, TravelDirection.Reverse, true), Point(0.4d, 0.4167d, TravelDirection.Reverse), + }, + new[] + { + new PathSegment(0, TravelDirection.Forward, 0, 1, false, true), + new PathSegment(1, TravelDirection.Reverse, 2, 3, true, false), + })); + return Snapshot(transitions); + } + + private static DetectionTestSnapshot DetectNoise() + { + IReadOnlyList transitions = Detect(CreateRequest( + new[] { Point(0d, 0d), Point(0.1d, 0.01d), Point(0.2d, 0.01d) }, + new[] { new PathSegment(0, TravelDirection.Forward, 0, 2, false, false) })); + return Snapshot(transitions); + } + + private static DetectionTestSnapshot PlanOverlap() + { + LocalG2OptionsSnapshot options = CreateOptions(); + var transitions = new[] + { + new CurvatureTransition(0, 1, 2, 0.4d, 0.4d, 0d, 0d, 0d, 0.5d), + new CurvatureTransition(0, 2, 3, 0.7d, 0.7d, 0d, 0d, 0.5d, 0d), + }; + var planner = new LocalG2WindowPlanner(); + if (!planner.TryPlan(CreatePreparedPath(1.4d), transitions, options, out IReadOnlyList regions, out string reason)) + throw new InvalidOperationException(reason); + return new DetectionTestSnapshot(2, 0.5d, regions.Count, regions[0].Transitions.Count, 0d, 0d, 0d); + } + + private static DetectionTestSnapshot PlanNearStart() + { + LocalG2OptionsSnapshot options = CreateOptions(); + var transitions = new[] { new CurvatureTransition(0, 0, 1, 0.1d, 0.1d, 0d, 0d, 0d, 0.5d) }; + var planner = new LocalG2WindowPlanner(); + if (!planner.TryPlan(CreatePreparedPath(1d), transitions, options, out IReadOnlyList regions, out string reason)) + throw new InvalidOperationException(reason); + LocalG2WindowVariant first = regions[0].WindowVariants[0]; + return new DetectionTestSnapshot(1, 0.5d, regions.Count, 1, + first.StartArcLengthMeters, first.LeftWindowLengthMeters, first.RightWindowLengthMeters); + } + + private static IReadOnlyList Detect(PathSmoothingRequest request) + { + var detector = new CurvatureTransitionDetector(); + if (!detector.TryDetect(request, 0.8333d, CreateOptions(), out IReadOnlyList transitions, out string reason)) + throw new InvalidOperationException(reason); + return transitions; + } + + private static DetectionTestSnapshot Snapshot(IReadOnlyList transitions) + { + double maximum = 0d; + for (int index = 0; index < transitions.Count; index++) maximum = Math.Max(maximum, transitions[index].CurvatureJumpPerMeter); + return new DetectionTestSnapshot(transitions.Count, maximum, 0, 0, 0d, 0d, 0d); + } + + private static PathSmoothingRequest CreateRequest(IReadOnlyList points, IReadOnlyList segments) + { + return new PathSmoothingRequest(points, segments, null, null, new PathSmoothingConfiguration()); + } + + private static CoarsePathPoint Point(double arcLength, double curvature, TravelDirection direction = TravelDirection.Forward, bool gearSwitch = false) + { + return new CoarsePathPoint(arcLength, 0d, 0d, 0d, arcLength, direction, curvature, 1d, gearSwitch, + CoarsePathPointSource.MotionPrimitive); + } + + private static LocalG2OptionsSnapshot CreateOptions() => new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()); + + private static Processing.PreparedPath CreatePreparedPath(double length) + { + var points = new[] + { + new Processing.SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new Processing.SmoothingPoint2D(length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + }; + var segment = new Processing.PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); + return new Processing.PreparedPath(new[] { segment }); + } + } + + private static bool ValidatePath(IReadOnlyList path, out string reason) + { + reason = string.Empty; + if (path.Count == 0) + { + reason = "局部 G2 曲率事件检测需要粗路径点。"; + return false; + } + double previousArc = -1d; + for (int index = 0; index < path.Count; index++) + { + CoarsePathPoint point = path[index]; + if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || + !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || + !NumericGuard.IsFinite(point.VehicleCurvature) || point.ArcLength < previousArc) + { + reason = "局部 G2 曲率事件检测要求有限且非递减的粗路径。"; + return false; + } + previousArc = point.ArcLength; + } + return true; + } + + private static bool ValidateSegments(IReadOnlyList path, IReadOnlyList segments, out string reason) + { + reason = string.Empty; + for (int position = 0; position < segments.Count; position++) + { + PathSegment segment = segments[position]; + if (segment == null || segment.SegmentIndex != position || segment.StartIndex < 0 || + segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count) + { + reason = "局部 G2 曲率事件检测的方向分段无效。"; + return false; + } + for (int index = segment.StartIndex; index <= segment.EndIndex; index++) + { + if (path[index].Direction != segment.Direction) + { + reason = "局部 G2 曲率事件检测的方向分段包含换向点。"; + return false; + } + } + } + return true; + } + + private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs new file mode 100644 index 0000000..2a9e207 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs @@ -0,0 +1,1242 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 以固定窗口和尺度顺序构造有限的局部五次 Hermite G2 候选。 +internal sealed class LocalG2CandidateBuilder +{ + private const double MinimumDerivativeNorm = 1e-10d; + private const int MaximumDerivativeCertificationDepth = 40; + private const int MaximumDerivativeCertificationIntervals = 8192; + private const double DerivativeCertificationMargin = 1e-12d; + private const int MaximumSubdivisionDepth = 32; + private static readonly double[] DerivativeScaleMultipliers = { 1.00d, 1.20d, 1.40d, 1.60d }; + private static readonly double[] InternalCurvatureFractions = { 0d, 0.10d, 0.20d }; + + internal IReadOnlyList Build( + PreparedDirectionSegment originalSegment, + LocalG2SmoothingRegion region, + double outputSpacingMeters, + LocalG2OptionsSnapshot options, + CancellationToken cancellationToken) + { + if (!IsValidInput(originalSegment, region, outputSpacingMeters, options)) return Empty(); + + var candidates = new List(); + for (int windowIndex = 0; windowIndex < region.WindowVariants.Count; windowIndex++) + { + LocalG2WindowVariant window = region.WindowVariants[windowIndex]; + bool hasInternalAnchor = HasInternalAnchor(region, window); + int curvatureCount = hasInternalAnchor ? InternalCurvatureFractions.Length : 1; + int scaleCount = hasInternalAnchor ? DerivativeScaleMultipliers.Length : 1; + for (int curvatureIndex = 0; curvatureIndex < curvatureCount; curvatureIndex++) + { + for (int scaleIndex = 0; scaleIndex < scaleCount; scaleIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + if (candidates.Count >= Math.Min(options.MaximumCandidatesPerRegion, 12)) return ReadOnly(candidates); + if (TryBuildCandidate( + curvatureIndex * scaleCount + scaleIndex, + originalSegment, + region, + window, + DerivativeScaleMultipliers[scaleIndex], + hasInternalAnchor ? InternalCurvatureFractions[curvatureIndex] : 0.5d, + outputSpacingMeters, + cancellationToken, + out LocalG2CandidateGeometry candidate)) + { + candidates.Add(candidate); + } + } + } + } + return ReadOnly(candidates); + } + + private static bool TryBuildCandidate( + int candidateIndex, + PreparedDirectionSegment segment, + LocalG2SmoothingRegion region, + LocalG2WindowVariant window, + double multiplier, + double internalCurvatureFraction, + double outputSpacingMeters, + CancellationToken cancellationToken, + out LocalG2CandidateGeometry candidate) + { + candidate = null; + if (!PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, window.StartArcLengthMeters, out SmoothingPoint2D startReference, out _) || + !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, window.EndArcLengthMeters, out SmoothingPoint2D endReference, out _)) + { + return false; + } + + double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; + CurvatureTransition first = region.Transitions[0]; + CurvatureTransition last = region.Transitions[region.Transitions.Count - 1]; + bool firstAtStart = SameArc(first.LocalArcLengthMeters, window.StartArcLengthMeters); + bool lastAtEnd = SameArc(last.LocalArcLengthMeters, window.EndArcLengthMeters); + double startVehicleCurvature = firstAtStart && segment.StartVehicleCurvaturePerMeter.HasValue + ? segment.StartVehicleCurvaturePerMeter.Value + : first.LeftVehicleCurvaturePerMeter; + double endVehicleCurvature = last.RightVehicleCurvaturePerMeter; + if (!NumericGuard.IsFinite(startVehicleCurvature) || !NumericGuard.IsFinite(endVehicleCurvature)) return false; + + var nodes = new List(); + nodes.Add(new BoundaryNode(window.StartArcLengthMeters, startReference.X, startReference.Y, + startReference.Heading, startVehicleCurvature)); + for (int transitionIndex = 0; transitionIndex < region.Transitions.Count; transitionIndex++) + { + CurvatureTransition transition = region.Transitions[transitionIndex]; + bool atStart = SameArc(transition.LocalArcLengthMeters, window.StartArcLengthMeters); + bool atEnd = SameArc(transition.LocalArcLengthMeters, window.EndArcLengthMeters); + // A one-sided transition at a hard segment boundary has no left/right zero-length piece. + if (atStart || atEnd) continue; + double leftAvailable = transition.LocalArcLengthMeters - window.StartArcLengthMeters; + double rightAvailable = window.EndArcLengthMeters - transition.LocalArcLengthMeters; + double denominator = leftAvailable + rightAvailable; + if (!NumericGuard.IsPositiveFinite(denominator)) return false; + double sharedCurvature = transition.LeftVehicleCurvaturePerMeter + + (transition.RightVehicleCurvaturePerMeter - transition.LeftVehicleCurvaturePerMeter) * internalCurvatureFraction; + if (!NumericGuard.IsFinite(sharedCurvature)) return false; + nodes.Add(new BoundaryNode(transition.LocalArcLengthMeters, transition.X, transition.Y, + transition.VehicleHeadingRadians, sharedCurvature)); + } + nodes.Add(new BoundaryNode(window.EndArcLengthMeters, endReference.X, endReference.Y, + endReference.Heading, endVehicleCurvature)); + + if (!TryAssignDerivativeScales(nodes, multiplier) || nodes.Count < 2) return false; + var sampled = new List(); + for (int nodeIndex = 1; nodeIndex < nodes.Count; nodeIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + BoundaryNode left = nodes[nodeIndex - 1]; + BoundaryNode right = nodes[nodeIndex]; + if (!TryCreateCurve(left, right, directionSign, out QuinticHermiteCurve2D curve)) return false; + if (!TryAppendCurveSamples( + curve, + left, + right, + segment, + outputSpacingMeters, + sampled, + cancellationToken)) + { + return false; + } + } + + if (sampled.Count < 2) return false; + candidate = new LocalG2CandidateGeometry( + candidateIndex, + segment.SegmentIndex, + window.StartArcLengthMeters, + window.EndArcLengthMeters, + window.LeftWindowLengthMeters, + window.RightWindowLengthMeters, + sampled, + startVehicleCurvature, + endVehicleCurvature, + directionSign * startVehicleCurvature, + directionSign * endVehicleCurvature, + true); + return true; + } + + private static bool HasInternalAnchor(LocalG2SmoothingRegion region, LocalG2WindowVariant window) + { + for (int index = 0; index < region.Transitions.Count; index++) + { + double arc = region.Transitions[index].LocalArcLengthMeters; + if (!SameArc(arc, window.StartArcLengthMeters) && !SameArc(arc, window.EndArcLengthMeters)) return true; + } + return false; + } + + private static bool TryAssignDerivativeScales(List nodes, double multiplier) + { + if (!NumericGuard.IsPositiveFinite(multiplier)) return false; + for (int index = 0; index < nodes.Count; index++) + { + double scale; + if (index == 0) scale = nodes[1].ArcLengthMeters - nodes[0].ArcLengthMeters; + else if (index == nodes.Count - 1) scale = nodes[index].ArcLengthMeters - nodes[index - 1].ArcLengthMeters; + else scale = ((nodes[index].ArcLengthMeters - nodes[index - 1].ArcLengthMeters) + + (nodes[index + 1].ArcLengthMeters - nodes[index].ArcLengthMeters)) / 2d; + if (index > 0 && index < nodes.Count - 1) scale *= multiplier; + if (!NumericGuard.IsPositiveFinite(scale)) return false; + nodes[index] = nodes[index].WithDerivativeScale(scale); + } + return true; + } + + private static bool TryCreateCurve(BoundaryNode left, BoundaryNode right, double directionSign, out QuinticHermiteCurve2D curve) + { + curve = null; + if (!TryGetDerivatives(left, directionSign, out double ldx, out double ldy, out double lddx, out double lddy) || + !TryGetDerivatives(right, directionSign, out double rdx, out double rdy, out double rddx, out double rddy)) + { + return false; + } + return QuinticHermiteCurve2D.TryCreate(left.X, left.Y, ldx, ldy, lddx, lddy, + right.X, right.Y, rdx, rdy, rddx, rddy, out curve, out _); + } + + private static bool TryGetDerivatives(BoundaryNode node, double directionSign, + out double dx, out double dy, out double ddx, out double ddy) + { + dx = dy = ddx = ddy = 0d; + double travelHeading = directionSign > 0d ? node.VehicleHeadingRadians : node.VehicleHeadingRadians - Math.PI; + double geometricCurvature = directionSign * node.VehicleCurvaturePerMeter; + double tx = Math.Cos(travelHeading); + double ty = Math.Sin(travelHeading); + double nx = -ty; + double ny = tx; + dx = node.DerivativeScale * tx; + dy = node.DerivativeScale * ty; + ddx = node.DerivativeScale * node.DerivativeScale * geometricCurvature * nx; + ddy = node.DerivativeScale * node.DerivativeScale * geometricCurvature * ny; + return NumericGuard.IsFinite(dx) && NumericGuard.IsFinite(dy) && NumericGuard.IsFinite(ddx) && NumericGuard.IsFinite(ddy) && + Math.Sqrt(dx * dx + dy * dy) >= MinimumDerivativeNorm; + } + + private static bool TryAppendCurveSamples( + QuinticHermiteCurve2D curve, + BoundaryNode left, + BoundaryNode right, + PreparedDirectionSegment segment, + double outputSpacingMeters, + List output, + CancellationToken cancellationToken) + { + // A short endpoint chord and a finite sample grid cannot rule out an interior cusp. + // Certify a derivative-norm lower bound over the whole parameter interval first. + if (!TryCertifyDerivativeLowerBound(curve)) return false; + if (!TryEvaluate(curve, 0d, out CurveSample start) || !TryEvaluate(curve, 1d, out CurveSample end)) return false; + if (output.Count == 0 && !TryAppendPoint(start, left, right, segment, output)) return false; + return TrySubdivide(curve, left, right, segment, outputSpacingMeters, 0d, start, 1d, end, 0, output, cancellationToken); + } + + private static bool TrySubdivide( + QuinticHermiteCurve2D curve, + BoundaryNode left, + BoundaryNode right, + PreparedDirectionSegment segment, + double outputSpacingMeters, + double u0, + CurveSample sample0, + double u1, + CurveSample sample1, + int depth, + List output, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (Distance(sample0, sample1) <= outputSpacingMeters) + { + return TryAppendPoint(sample1, left, right, segment, output); + } + if (depth >= MaximumSubdivisionDepth) return false; + double middle = (u0 + u1) / 2d; + if (!TryEvaluate(curve, middle, out CurveSample middleSample)) return false; + return TrySubdivide(curve, left, right, segment, outputSpacingMeters, u0, sample0, middle, middleSample, + depth + 1, output, cancellationToken) && + TrySubdivide(curve, left, right, segment, outputSpacingMeters, middle, middleSample, u1, sample1, + depth + 1, output, cancellationToken); + } + + private static bool TryAppendPoint( + CurveSample sample, + BoundaryNode left, + BoundaryNode right, + PreparedDirectionSegment segment, + List output) + { + double referenceArcLength = left.ArcLengthMeters + (right.ArcLengthMeters - left.ArcLengthMeters) * sample.Parameter; + if (!PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, referenceArcLength, out SmoothingPoint2D reference, out _)) + return false; + double x = SameArc(sample.Parameter, 0d) ? left.X : SameArc(sample.Parameter, 1d) ? right.X : sample.X; + double y = SameArc(sample.Parameter, 0d) ? left.Y : SameArc(sample.Parameter, 1d) ? right.Y : sample.Y; + double travelHeading = Math.Atan2(sample.Dy, sample.Dx); + double vehicleHeading = segment.Direction == TravelDirection.Forward ? travelHeading : travelHeading + Math.PI; + if (!NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(travelHeading) || !NumericGuard.IsFinite(vehicleHeading)) return false; + if (output.Count > 0 && Distance(output[output.Count - 1], x, y) <= 1e-12d) return false; + output.Add(new SmoothingPoint2D( + x, + y, + referenceArcLength, + AngleMath.NormalizeRadians(vehicleHeading), + vehicleHeading, + reference.BodyClearance, + false, + SmoothedPathPointSource.LocalG2Transition)); + return true; + } + + private static bool TryEvaluate(QuinticHermiteCurve2D curve, double parameter, out CurveSample sample) + { + sample = default; + curve.Evaluate(parameter, out double x, out double y, out double dx, out double dy, out double ddx, out double ddy); + double derivativeNorm = Math.Sqrt(dx * dx + dy * dy); + if (!NumericGuard.IsFinite(x) || !NumericGuard.IsFinite(y) || !NumericGuard.IsFinite(dx) || !NumericGuard.IsFinite(dy) || + !NumericGuard.IsFinite(ddx) || !NumericGuard.IsFinite(ddy) || !NumericGuard.IsFinite(derivativeNorm) || + derivativeNorm < MinimumDerivativeNorm) + { + return false; + } + sample = new CurveSample(parameter, x, y, dx, dy); + return true; + } + + private static bool TryCertifyDerivativeLowerBound(QuinticHermiteCurve2D curve) + { + if (!TryCreateDerivativeBezierControls(curve, out DerivativeControlPoint d0, out DerivativeControlPoint d1, + out DerivativeControlPoint d2, out DerivativeControlPoint d3, out DerivativeControlPoint d4)) + { + return false; + } + int visitedIntervals = 0; + return TryCertifyDerivativeLowerBound(d0, d1, d2, d3, d4, 0, ref visitedIntervals); + } + + private static bool TryCreateDerivativeBezierControls( + QuinticHermiteCurve2D curve, + out DerivativeControlPoint d0, + out DerivativeControlPoint d1, + out DerivativeControlPoint d2, + out DerivativeControlPoint d3, + out DerivativeControlPoint d4) + { + d0 = d1 = d2 = d3 = d4 = default; + curve.Evaluate(0d, out _, out _, out double dx0, out double dy0, out double ddx0, out double ddy0); + curve.Evaluate(0.5d, out _, out _, out double dxMiddle, out double dyMiddle, out _, out _); + curve.Evaluate(1d, out _, out _, out double dxEnd, out double dyEnd, out double ddxEnd, out double ddyEnd); + d0 = new DerivativeControlPoint(dx0, dy0); + d1 = new DerivativeControlPoint(dx0 + ddx0 / 4d, dy0 + ddy0 / 4d); + d4 = new DerivativeControlPoint(dxEnd, dyEnd); + d3 = new DerivativeControlPoint(dxEnd - ddxEnd / 4d, dyEnd - ddyEnd / 4d); + d2 = new DerivativeControlPoint( + (16d * dxMiddle - d0.X - 4d * d1.X - 4d * d3.X - d4.X) / 6d, + (16d * dyMiddle - d0.Y - 4d * d1.Y - 4d * d3.Y - d4.Y) / 6d); + return d0.IsFinite && d1.IsFinite && d2.IsFinite && d3.IsFinite && d4.IsFinite; + } + + private static bool TryCertifyDerivativeLowerBound( + DerivativeControlPoint d0, + DerivativeControlPoint d1, + DerivativeControlPoint d2, + DerivativeControlPoint d3, + DerivativeControlPoint d4, + int depth, + ref int visitedIntervals) + { + if (++visitedIntervals > MaximumDerivativeCertificationIntervals) return false; + if (!TryGetOriginToConvexHullDistance(d0, d1, d2, d3, d4, out double lowerBound, out double margin)) + return false; + if (lowerBound > MinimumDerivativeNorm + margin) return true; + if (depth >= MaximumDerivativeCertificationDepth) return false; + + DerivativeControlPoint d01 = Midpoint(d0, d1); + DerivativeControlPoint d12 = Midpoint(d1, d2); + DerivativeControlPoint d23 = Midpoint(d2, d3); + DerivativeControlPoint d34 = Midpoint(d3, d4); + DerivativeControlPoint d012 = Midpoint(d01, d12); + DerivativeControlPoint d123 = Midpoint(d12, d23); + DerivativeControlPoint d234 = Midpoint(d23, d34); + DerivativeControlPoint d0123 = Midpoint(d012, d123); + DerivativeControlPoint d1234 = Midpoint(d123, d234); + DerivativeControlPoint middle = Midpoint(d0123, d1234); + if (!d01.IsFinite || !d12.IsFinite || !d23.IsFinite || !d34.IsFinite || !d012.IsFinite || !d123.IsFinite || + !d234.IsFinite || !d0123.IsFinite || !d1234.IsFinite || !middle.IsFinite) + { + return false; + } + + return TryCertifyDerivativeLowerBound(d0, d01, d012, d0123, middle, depth + 1, ref visitedIntervals) && + TryCertifyDerivativeLowerBound(middle, d1234, d234, d34, d4, depth + 1, ref visitedIntervals); + } + + private static bool TryGetOriginToConvexHullDistance( + DerivativeControlPoint d0, + DerivativeControlPoint d1, + DerivativeControlPoint d2, + DerivativeControlPoint d3, + DerivativeControlPoint d4, + out double distance, + out double margin) + { + distance = 0d; + margin = 0d; + var controls = new[] { d0, d1, d2, d3, d4 }; + double maximumCoordinate = 0d; + for (int index = 0; index < controls.Length; index++) + { + if (!controls[index].IsFinite) return false; + maximumCoordinate = Math.Max(maximumCoordinate, Math.Max(Math.Abs(controls[index].X), Math.Abs(controls[index].Y))); + } + if (!NumericGuard.IsFinite(maximumCoordinate)) return false; + margin = DerivativeCertificationMargin * Math.Max(1d, maximumCoordinate); + + for (int first = 0; first < controls.Length - 2; first++) + { + for (int second = first + 1; second < controls.Length - 1; second++) + { + for (int third = second + 1; third < controls.Length; third++) + { + if (ContainsOrigin(controls[first], controls[second], controls[third], margin)) return true; + } + } + } + + distance = double.PositiveInfinity; + for (int first = 0; first < controls.Length; first++) + { + distance = Math.Min(distance, controls[first].Norm); + for (int second = first + 1; second < controls.Length; second++) + distance = Math.Min(distance, DistanceToSegment(controls[first], controls[second])); + } + return NumericGuard.IsFinite(distance); + } + + private static bool ContainsOrigin(DerivativeControlPoint first, DerivativeControlPoint second, + DerivativeControlPoint third, double margin) + { + double sideX = second.X - first.X; + double sideY = second.Y - first.Y; + double thirdOffsetX = third.X - first.X; + double thirdOffsetY = third.Y - first.Y; + double triangleArea = sideX * thirdOffsetY - sideY * thirdOffsetX; + double crossFirstSecond = Cross(first, second); + double crossSecondThird = Cross(second, third); + double crossThirdFirst = Cross(third, first); + if (!NumericGuard.IsFinite(triangleArea) || !NumericGuard.IsFinite(crossFirstSecond) || + !NumericGuard.IsFinite(crossSecondThird) || !NumericGuard.IsFinite(crossThirdFirst)) + { + return true; + } + double areaMargin = margin * Math.Max(1d, Math.Max(first.Norm, Math.Max(second.Norm, third.Norm))); + if (!NumericGuard.IsFinite(areaMargin)) return true; + if (Math.Abs(triangleArea) <= areaMargin) + { + // A degenerate triangle is only a closed line segment (or a point), not a + // two-dimensional region. Treat it as origin-containing only when the origin + // lies on one of its actual closed segments; otherwise its distance is positive. + return IsOriginOnSegment(first, second, margin) || IsOriginOnSegment(second, third, margin) || + IsOriginOnSegment(third, first, margin); + } + return (crossFirstSecond >= -areaMargin && crossSecondThird >= -areaMargin && crossThirdFirst >= -areaMargin) || + (crossFirstSecond <= areaMargin && crossSecondThird <= areaMargin && crossThirdFirst <= areaMargin); + } + + private static bool IsOriginOnSegment(DerivativeControlPoint start, DerivativeControlPoint end, double margin) + { + double dx = end.X - start.X; + double dy = end.Y - start.Y; + double lengthSquared = dx * dx + dy * dy; + if (!NumericGuard.IsFinite(lengthSquared)) return true; + if (lengthSquared == 0d) return start.Norm <= margin; + double length = Math.Sqrt(lengthSquared); + double cross = Cross(start, end); + double projection = -(start.X * dx + start.Y * dy); + if (!NumericGuard.IsFinite(length) || !NumericGuard.IsFinite(cross) || !NumericGuard.IsFinite(projection)) return true; + double lineMargin = margin * Math.Max(1d, length); + double projectionMargin = margin * Math.Max(1d, length); + if (!NumericGuard.IsFinite(lineMargin) || !NumericGuard.IsFinite(projectionMargin)) return true; + return Math.Abs(cross) <= lineMargin && projection >= -projectionMargin && + projection <= lengthSquared + projectionMargin; + } + + private static double DistanceToSegment(DerivativeControlPoint start, DerivativeControlPoint end) + { + double dx = end.X - start.X; + double dy = end.Y - start.Y; + double denominator = dx * dx + dy * dy; + if (!NumericGuard.IsFinite(denominator)) return double.NaN; + if (denominator == 0d) return start.Norm; + double parameter = -(start.X * dx + start.Y * dy) / denominator; + if (!NumericGuard.IsFinite(parameter)) return double.NaN; + parameter = Math.Max(0d, Math.Min(1d, parameter)); + double x = start.X + parameter * dx; + double y = start.Y + parameter * dy; + return Math.Sqrt(x * x + y * y); + } + + private static DerivativeControlPoint Midpoint(DerivativeControlPoint left, DerivativeControlPoint right) => + new DerivativeControlPoint((left.X + right.X) / 2d, (left.Y + right.Y) / 2d); + private static double Cross(DerivativeControlPoint left, DerivativeControlPoint right) => left.X * right.Y - left.Y * right.X; + + private static bool IsValidInput(PreparedDirectionSegment segment, LocalG2SmoothingRegion region, + double outputSpacingMeters, LocalG2OptionsSnapshot options) + { + if (segment == null || region == null || options == null || segment.SegmentIndex != region.SegmentIndex || + !NumericGuard.IsPositiveFinite(outputSpacingMeters) || segment.Points == null || segment.Points.Count < 2 || + region.Transitions == null || region.Transitions.Count == 0 || region.WindowVariants == null) + { + return false; + } + for (int index = 0; index < region.Transitions.Count; index++) + { + CurvatureTransition transition = region.Transitions[index]; + if (transition == null || transition.SegmentIndex != segment.SegmentIndex) return false; + } + return true; + } + + private static double Distance(CurveSample left, CurveSample right) => Distance(left.X, left.Y, right.X, right.Y); + private static double Distance(SmoothingPoint2D point, double x, double y) => Distance(point.X, point.Y, x, y); + private static double Distance(double x0, double y0, double x1, double y1) + { + double dx = x1 - x0; + double dy = y1 - y0; + return Math.Sqrt(dx * dx + dy * dy); + } + private static bool SameArc(double left, double right) => Math.Abs(left - right) <= 1e-10d; + private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); + private static IReadOnlyList ReadOnly(List values) => + new ReadOnlyCollection(values); + + private readonly struct BoundaryNode + { + internal BoundaryNode(double arcLengthMeters, double x, double y, double vehicleHeadingRadians, double vehicleCurvaturePerMeter) + { + ArcLengthMeters = arcLengthMeters; + X = x; + Y = y; + VehicleHeadingRadians = vehicleHeadingRadians; + VehicleCurvaturePerMeter = vehicleCurvaturePerMeter; + DerivativeScale = 0d; + } + private BoundaryNode(double arcLengthMeters, double x, double y, double vehicleHeadingRadians, + double vehicleCurvaturePerMeter, double derivativeScale) + { + ArcLengthMeters = arcLengthMeters; + X = x; + Y = y; + VehicleHeadingRadians = vehicleHeadingRadians; + VehicleCurvaturePerMeter = vehicleCurvaturePerMeter; + DerivativeScale = derivativeScale; + } + internal double ArcLengthMeters { get; } + internal double X { get; } + internal double Y { get; } + internal double VehicleHeadingRadians { get; } + internal double VehicleCurvaturePerMeter { get; } + internal double DerivativeScale { get; } + internal BoundaryNode WithDerivativeScale(double derivativeScale) => new BoundaryNode( + ArcLengthMeters, X, Y, VehicleHeadingRadians, VehicleCurvaturePerMeter, derivativeScale); + } + + private readonly struct CurveSample + { + internal CurveSample(double parameter, double x, double y, double dx, double dy) + { + Parameter = parameter; + X = x; + Y = y; + Dx = dx; + Dy = dy; + } + internal double Parameter { get; } + internal double X { get; } + internal double Y { get; } + internal double Dx { get; } + internal double Dy { get; } + } + + private readonly struct DerivativeControlPoint + { + internal DerivativeControlPoint(double x, double y) + { + X = x; + Y = y; + } + internal double X { get; } + internal double Y { get; } + internal bool IsFinite => NumericGuard.IsFinite(X) && NumericGuard.IsFinite(Y); + internal double Norm => Math.Sqrt(X * X + Y * Y); + } + + /// 反射脚本使用的窄范围候选与拼接场景入口。 + public static class TestHooks + { + public static CandidateTestSnapshot Execute(string scenario) + { + if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario)); + switch (scenario) + { + case "Isolated": return BuildIsolated(); + case "Cluster": return BuildCluster(); + case "Reverse": return BuildReverse(); + case "Spliced": return BuildSpliced(); + case "StartBoundary": return BuildStartBoundary(); + case "InteriorStationaryCurve": return BuildInteriorStationaryCurve(); + case "ConstantVelocityCurve": return BuildConstantVelocityCurve(); + case "ExactSpliceEndpoints": return BuildExactSpliceEndpoints(); + case "GearBoundary": return BuildGearBoundary(); + case "TwoRegionWorkOrder": return BuildTwoRegionWorkOrder(); + case "MultiSegmentWorkOrder": return BuildMultiSegmentWorkOrder(); + default: throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + + public sealed class CandidateTestSnapshot + { + internal CandidateTestSnapshot(int candidateCount, double startPositionError, double endPositionError, + double startCurvatureError, double endCurvatureError, bool containsLocalG2Source, + int outputRegionCount, bool internalConnectionsAreG2, string direction, + bool vehicleAndGeometricCurvatureSignsAreOpposite, bool noDuplicateNonGearPoints, + bool endpointsUnchanged, bool rejected = false, bool endpointsAreExact = false, + bool gearBoundaryMarkerPreserved = false, bool accepted = false, + bool workOrderDescending = false, bool frontArcPreservedAfterBackReplacement = false, + bool bothReplacementsRetained = false, bool forwardOrderRejected = false, + bool deterministicWorkOrder = false, bool invalidWorkOrderRejected = false, + bool deterministicReplacementGeometry = false, bool segmentOrderAscending = false, + bool equalArcUsesReportOrder = false) + { + CandidateCount = candidateCount; + StartPositionError = startPositionError; + EndPositionError = endPositionError; + StartCurvatureError = startCurvatureError; + EndCurvatureError = endCurvatureError; + ContainsLocalG2Source = containsLocalG2Source; + OutputRegionCount = outputRegionCount; + InternalConnectionsAreG2 = internalConnectionsAreG2; + Direction = direction; + VehicleAndGeometricCurvatureSignsAreOpposite = vehicleAndGeometricCurvatureSignsAreOpposite; + NoDuplicateNonGearPoints = noDuplicateNonGearPoints; + EndpointsUnchanged = endpointsUnchanged; + Rejected = rejected; + EndpointsAreExact = endpointsAreExact; + GearBoundaryMarkerPreserved = gearBoundaryMarkerPreserved; + Accepted = accepted; + WorkOrderDescending = workOrderDescending; + FrontArcPreservedAfterBackReplacement = frontArcPreservedAfterBackReplacement; + BothReplacementsRetained = bothReplacementsRetained; + ForwardOrderRejected = forwardOrderRejected; + DeterministicWorkOrder = deterministicWorkOrder; + InvalidWorkOrderRejected = invalidWorkOrderRejected; + DeterministicReplacementGeometry = deterministicReplacementGeometry; + SegmentOrderAscending = segmentOrderAscending; + EqualArcUsesReportOrder = equalArcUsesReportOrder; + } + public int CandidateCount { get; } + public double StartPositionError { get; } + public double EndPositionError { get; } + public double StartCurvatureError { get; } + public double EndCurvatureError { get; } + public bool ContainsLocalG2Source { get; } + public int OutputRegionCount { get; } + public bool InternalConnectionsAreG2 { get; } + public string Direction { get; } + public bool VehicleAndGeometricCurvatureSignsAreOpposite { get; } + public bool NoDuplicateNonGearPoints { get; } + public bool EndpointsUnchanged { get; } + public bool Rejected { get; } + public bool EndpointsAreExact { get; } + public bool GearBoundaryMarkerPreserved { get; } + public bool Accepted { get; } + public bool WorkOrderDescending { get; } + public bool FrontArcPreservedAfterBackReplacement { get; } + public bool BothReplacementsRetained { get; } + public bool ForwardOrderRejected { get; } + public bool DeterministicWorkOrder { get; } + public bool InvalidWorkOrderRejected { get; } + public bool DeterministicReplacementGeometry { get; } + public bool SegmentOrderAscending { get; } + public bool EqualArcUsesReportOrder { get; } + } + + private static CandidateTestSnapshot BuildIsolated() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); + LocalG2SmoothingRegion region = CreateRegion(new[] + { + new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0d, 0.4d), + }, 0.25d, 0.75d); + IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( + segment, region, 0.025d, CreateOptions(), CancellationToken.None); + if (candidates.Count == 0) throw new InvalidOperationException("The isolated candidate scenario produced no candidate."); + LocalG2CandidateGeometry candidate = candidates[0]; + PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.25d, out SmoothingPoint2D start, out _); + PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.75d, out SmoothingPoint2D end, out _); + return new CandidateTestSnapshot(candidates.Count, + Distance(start, candidate.RegionPoints[0]), Distance(end, candidate.RegionPoints[candidate.RegionPoints.Count - 1]), + Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0d), + Math.Abs(candidate.EndVehicleCurvaturePerMeter - 0.4d), + ContainsLocalG2(candidate.RegionPoints), 0, false, string.Empty, false, false, false); + } + + private static CandidateTestSnapshot BuildCluster() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); + var transitions = new[] + { + new CurvatureTransition(0, 1, 2, 0.4d, 0.4d, 0d, 0d, 0d, 0.4d), + new CurvatureTransition(0, 2, 3, 0.6d, 0.6d, 0d, 0d, 0.4d, 0d), + }; + var planner = new LocalG2WindowPlanner(); + if (!planner.TryPlan(new PreparedPath(new[] { segment }), transitions, CreateOptions(), + out IReadOnlyList regions, out string reason)) + { + throw new InvalidOperationException(reason); + } + IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( + segment, regions[0], 0.025d, CreateOptions(), CancellationToken.None); + if (candidates.Count == 0) throw new InvalidOperationException("The clustered candidate scenario produced no candidate."); + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, regions.Count, + candidates[0].InternalConnectionsAreG2, string.Empty, false, false, false); + } + + private static CandidateTestSnapshot BuildReverse() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Reverse, null); + LocalG2SmoothingRegion region = CreateRegion(new[] + { + new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, Math.PI, 0.2d, 0.4d), + }, 0.25d, 0.75d); + IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( + segment, region, 0.025d, CreateOptions(), CancellationToken.None); + if (candidates.Count == 0) throw new InvalidOperationException("The reverse candidate scenario produced no candidate."); + LocalG2CandidateGeometry candidate = candidates[0]; + bool opposite = candidate.StartVehicleCurvaturePerMeter * candidate.StartGeometricCurvaturePerMeter < 0d && + candidate.EndVehicleCurvaturePerMeter * candidate.EndGeometricCurvaturePerMeter < 0d; + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, + segment.Direction.ToString(), opposite, false, false); + } + + private static CandidateTestSnapshot BuildSpliced() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, 0.17d); + LocalG2SmoothingRegion region = CreateRegion(new[] + { + new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0.17d, 0.4d), + }, 0.25d, 0.75d); + IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( + segment, region, 0.025d, CreateOptions(), CancellationToken.None); + if (candidates.Count == 0) throw new InvalidOperationException("The splice candidate scenario produced no candidate."); + PreparedDirectionSegment untouched = CreateSegment(TravelDirection.Reverse, null); + var path = new PreparedPath(new[] { segment, untouched }); + if (!new LocalG2PathSplicer().TryReplace(path, candidates[0], out PreparedPath replaced, out string reason)) + throw new InvalidOperationException(reason); + PreparedDirectionSegment result = replaced.Segments[0]; + bool noDuplicates = true; + for (int index = 1; index < result.Points.Count; index++) + { + if (!result.Points[index - 1].IsGearSwitchPoint && !result.Points[index].IsGearSwitchPoint && + Distance(result.Points[index - 1], result.Points[index]) <= 1e-12d) + { + noDuplicates = false; + break; + } + } + bool monotonicallyIncreasing = true; + for (int index = 1; index < result.Points.Count; index++) + { + if (result.Points[index].ArcLength <= result.Points[index - 1].ArcLength) + { + monotonicallyIncreasing = false; + break; + } + } + bool endpoints = Distance(segment.Points[0], result.Points[0]) <= 1e-12d && + Distance(segment.Points[segment.Points.Count - 1], result.Points[result.Points.Count - 1]) <= 1e-12d && + result.StartVehicleCurvaturePerMeter == segment.StartVehicleCurvaturePerMeter && monotonicallyIncreasing && + ReferenceEquals(untouched, replaced.Segments[1]); + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, + false, noDuplicates, endpoints); + } + + private static CandidateTestSnapshot BuildStartBoundary() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, 0.17d); + LocalG2SmoothingRegion region = CreateRegion(new[] + { + new CurvatureTransition(0, 0, 1, 0d, 0d, 0d, 0d, 0.17d, 0.4d), + }, 0d, 0.5d); + IReadOnlyList candidates = new LocalG2CandidateBuilder().Build( + segment, region, 0.025d, CreateOptions(), CancellationToken.None); + if (candidates.Count == 0) throw new InvalidOperationException("The start-boundary transition must not create a zero-length left curve."); + LocalG2CandidateGeometry candidate = candidates[0]; + return new CandidateTestSnapshot(candidates.Count, + Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0.17d), 0d, + Math.Abs(candidate.StartVehicleCurvaturePerMeter - 0.17d), 0d, + ContainsLocalG2(candidate.RegionPoints), 0, false, string.Empty, false, false, false); + } + + private static CandidateTestSnapshot BuildInteriorStationaryCurve() + { + const double endpointX = 0.00325d; + if (!QuinticHermiteCurve2D.TryCreate(0d, 0d, 0.01d, 0d, 0d, 0d, + endpointX, 0d, 0.01d, 0d, 0d, 0d, out QuinticHermiteCurve2D curve, out string reason)) + { + throw new InvalidOperationException(reason); + } + var points = new[] + { + new SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothingPoint2D(endpointX, 0d, endpointX, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + }; + var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); + var sampled = new List(); + bool accepted = TryAppendCurveSamples(curve, + new BoundaryNode(0d, 0d, 0d, 0d, 0d), + new BoundaryNode(endpointX, endpointX, 0d, 0d, 0d), + segment, 0.025d, sampled, CancellationToken.None); + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, + false, false, false, !accepted); + } + + private static CandidateTestSnapshot BuildConstantVelocityCurve() + { + if (!QuinticHermiteCurve2D.TryCreate(0d, 0d, 1d, 0d, 0d, 0d, + 1d, 0d, 1d, 0d, 0d, 0d, out QuinticHermiteCurve2D curve, out string reason)) + { + throw new InvalidOperationException(reason); + } + var points = new[] + { + new SmoothingPoint2D(0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothingPoint2D(1d, 0d, 1d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + }; + var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, points, false, false); + var sampled = new List(); + bool accepted = TryAppendCurveSamples(curve, + new BoundaryNode(0d, 0d, 0d, 0d, 0d), + new BoundaryNode(1d, 1d, 0d, 0d, 0d), + segment, 0.025d, sampled, CancellationToken.None); + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, + false, false, false, false, false, false, accepted); + } + + private static CandidateTestSnapshot BuildExactSpliceEndpoints() + { + PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); + var candidate = new LocalG2CandidateGeometry(0, 0, 0.25d, 0.75d, 0.25d, 0.25d, + new[] + { + Point(0.2500000005d, 0d, 0.25d, false), + Point(0.5d, 0d, 0.5d, false), + Point(0.7499999995d, 0d, 0.75d, false), + }, 0d, 0d, 0d, 0d, true); + if (!new LocalG2PathSplicer().TryReplace(new PreparedPath(new[] { segment }), candidate, + out PreparedPath replaced, out string reason)) + { + throw new InvalidOperationException(reason); + } + PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.25d, out SmoothingPoint2D start, out _); + PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, 0.75d, out SmoothingPoint2D end, out _); + IReadOnlyList result = replaced.Segments[0].Points; + bool exact = result[1].X == start.X && result[1].Y == start.Y && + result[result.Count - 2].X == end.X && result[result.Count - 2].Y == end.Y; + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, + false, false, false, false, exact); + } + + private static CandidateTestSnapshot BuildGearBoundary() + { + var sourcePoints = new[] + { + Point(0d, 0d, 0d, true), Point(0.25d, 0d, 0.25d, false), Point(0.5d, 0d, 0.5d, false), + }; + var segment = new PreparedDirectionSegment(0, TravelDirection.Forward, sourcePoints, true, false); + var candidate = new LocalG2CandidateGeometry(0, 0, 0d, 0.5d, 0d, 0.5d, + new[] { Point(0d, 0d, 0d, false), Point(0.25d, 0d, 0.25d, false), Point(0.5d, 0d, 0.5d, false) }, + 0d, 0d, 0d, 0d, true); + if (!new LocalG2PathSplicer().TryReplace(new PreparedPath(new[] { segment }), candidate, + out PreparedPath replaced, out string reason)) + { + throw new InvalidOperationException(reason); + } + bool preserved = replaced.Segments[0].StartsAtGearSwitch && replaced.Segments[0].Points[0].IsGearSwitchPoint; + return new CandidateTestSnapshot(0, 0d, 0d, 0d, 0d, false, 0, false, string.Empty, + false, false, false, false, false, preserved); + } + + private static CandidateTestSnapshot BuildTwoRegionWorkOrder() + { + var points = new List(); + for (int index = 0; index <= 12; index++) + { + double arc = index * 0.25d; + points.Add(new SmoothingPoint2D( + arc, + 0d, + arc, + 0d, + 0d, + 1d, + false, + SmoothedPathPointSource.Anchor)); + } + + var segment = new PreparedDirectionSegment( + 0, + TravelDirection.Forward, + points, + false, + false); + var original = new PreparedPath(new[] { segment }); + LocalG2SmoothingRegion frontRegion = CreateOrderedRegion(0.75d, 0.5d, 1.0d, 0); + LocalG2SmoothingRegion backRegion = CreateOrderedRegion(2.25d, 2.0d, 2.5d, 1); + var reportOrder = new[] { frontRegion, backRegion }; + + var orderer = new LocalG2RegionWorkOrder(); + if (!orderer.TryCreate( + reportOrder, + out IReadOnlyList workOrder, + out string orderReason)) + { + throw new InvalidOperationException(orderReason); + } + if (!orderer.TryCreate( + reportOrder, + out IReadOnlyList repeatedOrder, + out string repeatedReason)) + { + throw new InvalidOperationException(repeatedReason); + } + + bool descending = + ReferenceEquals(backRegion, workOrder[0]) && + ReferenceEquals(frontRegion, workOrder[1]); + bool deterministic = + ReferenceEquals(workOrder[0], repeatedOrder[0]) && + ReferenceEquals(workOrder[1], repeatedOrder[1]) && + ReferenceEquals(frontRegion, reportOrder[0]) && + ReferenceEquals(backRegion, reportOrder[1]); + + var wrongSegmentTransition = new CurvatureTransition( + 1, + 0, + 1, + 0.75d, + 0.75d, + 0d, + 0d, + 0d, + 0.4d); + var invalidRegion = new LocalG2SmoothingRegion( + 0, + new[] { wrongSegmentTransition }, + 0.5d, + 1.0d, + new[] + { + new LocalG2WindowVariant(0, 0.5d, 1.0d, 0.25d, 0.25d), + }); + bool invalidWorkOrderRejected = + !orderer.TryCreate( + new[] { invalidRegion }, + out _, + out _) && + !orderer.TryCreate( + new LocalG2SmoothingRegion[] { null }, + out _, + out _); + + LocalG2CandidateGeometry frontCandidate = + CreateLengthChangingCandidate(0, 0.5d, 1.0d); + LocalG2CandidateGeometry backCandidate = + CreateLengthChangingCandidate(1, 2.0d, 2.5d); + var splicer = new LocalG2PathSplicer(); + + if (!splicer.TryReplace( + original, + backCandidate, + out PreparedPath afterBack, + out string backReason)) + { + throw new InvalidOperationException(backReason); + } + + bool frontArcPreserved = + PathReferenceInterpolator.TryInterpolateByArcLength( + afterBack.Segments[0].Points, + 0.5d, + out SmoothingPoint2D frontStart, + out _) && + PathReferenceInterpolator.TryInterpolateByArcLength( + afterBack.Segments[0].Points, + 1.0d, + out SmoothingPoint2D frontEnd, + out _) && + Math.Abs(frontStart.X - 0.5d) <= 1e-12d && + Math.Abs(frontEnd.X - 1.0d) <= 1e-12d; + + if (!splicer.TryReplace( + afterBack, + frontCandidate, + out PreparedPath afterBoth, + out string frontReason)) + { + throw new InvalidOperationException(frontReason); + } + + if (!splicer.TryReplace( + original, + backCandidate, + out PreparedPath repeatedAfterBack, + out string repeatedBackReason)) + { + throw new InvalidOperationException(repeatedBackReason); + } + if (!splicer.TryReplace( + repeatedAfterBack, + frontCandidate, + out PreparedPath repeatedAfterBoth, + out string repeatedFrontReason)) + { + throw new InvalidOperationException(repeatedFrontReason); + } + + bool frontInteriorRetained = false; + bool backInteriorRetained = false; + int frontInteriorCount = 0; + int backInteriorCount = 0; + for (int index = 0; index < afterBoth.Segments[0].Points.Count; index++) + { + SmoothingPoint2D point = afterBoth.Segments[0].Points[index]; + if (point.Source != SmoothedPathPointSource.LocalG2Transition) + continue; + if (point.X == 0.75d && point.Y == 0.20d) + { + frontInteriorCount++; + frontInteriorRetained = true; + } + if (point.X == 2.25d && point.Y == 0.20d) + { + backInteriorCount++; + backInteriorRetained = true; + } + } + bool exactInteriorsRetained = + frontInteriorRetained && + backInteriorRetained && + frontInteriorCount == 1 && + backInteriorCount == 1; + bool deterministicReplacementGeometry = + HasSameReplacementGeometry(afterBoth, repeatedAfterBoth); + + if (!splicer.TryReplace( + original, + frontCandidate, + out PreparedPath afterFront, + out string firstReason)) + { + throw new InvalidOperationException(firstReason); + } + bool forwardOrderRejected = !splicer.TryReplace( + afterFront, + backCandidate, + out _, + out _); + + return new CandidateTestSnapshot( + 0, + 0d, + 0d, + 0d, + 0d, + false, + 0, + false, + string.Empty, + false, + false, + false, + false, + false, + false, + false, + descending, + frontArcPreserved, + exactInteriorsRetained, + forwardOrderRejected, + deterministic, + invalidWorkOrderRejected, + deterministicReplacementGeometry); + } + + private static CandidateTestSnapshot BuildMultiSegmentWorkOrder() + { + LocalG2SmoothingRegion laterSegment = + CreateOrderedRegion(0.25d, 0d, 0.5d, 0, 1); + LocalG2SmoothingRegion firstTie = + CreateOrderedRegion(0.75d, 0.5d, 1.0d, 0); + LocalG2SmoothingRegion secondTie = + CreateOrderedRegion(0.75d, 0.5d, 1.0d, 1); + var reportOrder = new[] { laterSegment, firstTie, secondTie }; + if (!new LocalG2RegionWorkOrder().TryCreate( + reportOrder, + out IReadOnlyList workOrder, + out string reason)) + { + throw new InvalidOperationException(reason); + } + + bool segmentOrderAscending = + workOrder.Count == 3 && + workOrder[0].SegmentIndex == 0 && + workOrder[1].SegmentIndex == 0 && + workOrder[2].SegmentIndex == 1; + bool equalArcUsesReportOrder = + ReferenceEquals(firstTie, workOrder[0]) && + ReferenceEquals(secondTie, workOrder[1]); + return new CandidateTestSnapshot( + 0, + 0d, + 0d, + 0d, + 0d, + false, + 0, + false, + string.Empty, + false, + false, + false, + segmentOrderAscending: segmentOrderAscending, + equalArcUsesReportOrder: equalArcUsesReportOrder); + } + + private static bool HasSameReplacementGeometry( + PreparedPath first, + PreparedPath second) + { + if (first == null || second == null || + first.Points.Count != second.Points.Count || + first.Segments.Count != second.Segments.Count) + { + return false; + } + + for (int segmentIndex = 0; segmentIndex < first.Segments.Count; segmentIndex++) + { + PreparedDirectionSegment firstSegment = first.Segments[segmentIndex]; + PreparedDirectionSegment secondSegment = second.Segments[segmentIndex]; + if (firstSegment.Direction != secondSegment.Direction || + firstSegment.Points.Count != secondSegment.Points.Count) + { + return false; + } + + for (int pointIndex = 0; pointIndex < firstSegment.Points.Count; pointIndex++) + { + SmoothingPoint2D firstPoint = firstSegment.Points[pointIndex]; + SmoothingPoint2D secondPoint = secondSegment.Points[pointIndex]; + if (firstPoint.X != secondPoint.X || + firstPoint.Y != secondPoint.Y || + firstPoint.Source != secondPoint.Source || + firstSegment.Direction != secondSegment.Direction) + { + return false; + } + } + } + return true; + } + + private static LocalG2SmoothingRegion CreateOrderedRegion( + double eventArc, + double startArc, + double endArc, + int index, + int segmentIndex = 0) + { + var transition = new CurvatureTransition( + segmentIndex, + index, + index + 1, + eventArc, + eventArc, + 0d, + 0d, + 0d, + 0.4d); + return new LocalG2SmoothingRegion( + segmentIndex, + new[] { transition }, + startArc, + endArc, + new[] + { + new LocalG2WindowVariant( + 0, + startArc, + endArc, + eventArc - startArc, + endArc - eventArc), + }); + } + + private static LocalG2CandidateGeometry CreateLengthChangingCandidate( + int candidateIndex, + double startArc, + double endArc) + { + double middleArc = 0.5d * (startArc + endArc); + var points = new[] + { + Point(startArc, 0d, startArc, false), + Point(middleArc, 0.20d, middleArc, false), + Point(endArc, 0d, endArc, false), + }; + return new LocalG2CandidateGeometry( + candidateIndex, + 0, + startArc, + endArc, + middleArc - startArc, + endArc - middleArc, + points, + 0d, + 0d, + 0d, + 0d, + true); + } + + private static PreparedDirectionSegment CreateSegment(TravelDirection direction, double? startCurvature) + { + double heading = direction == TravelDirection.Forward ? 0d : Math.PI; + var points = new List(); + for (int index = 0; index <= 4; index++) + { + double arc = index * 0.25d; + points.Add(new SmoothingPoint2D(arc, 0d, arc, heading, heading, 1d, false, SmoothedPathPointSource.Anchor)); + } + return new PreparedDirectionSegment(0, direction, points, false, false, startCurvature); + } + + private static LocalG2SmoothingRegion CreateRegion( + IReadOnlyList transitions, + double startArcLengthMeters, + double endArcLengthMeters) + { + return new LocalG2SmoothingRegion(0, transitions, startArcLengthMeters, endArcLengthMeters, + new[] { new LocalG2WindowVariant(0, startArcLengthMeters, endArcLengthMeters, + transitions[0].LocalArcLengthMeters - startArcLengthMeters, + endArcLengthMeters - transitions[transitions.Count - 1].LocalArcLengthMeters) }); + } + + private static LocalG2OptionsSnapshot CreateOptions() => new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()); + private static bool ContainsLocalG2(IReadOnlyList points) + { + for (int index = 0; index < points.Count; index++) + if (points[index].Source == SmoothedPathPointSource.LocalG2Transition) return true; + return false; + } + private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) => + Math.Sqrt((right.X - left.X) * (right.X - left.X) + (right.Y - left.Y) * (right.Y - left.Y)); + private static SmoothingPoint2D Point(double x, double y, double arcLength, bool isGearSwitch) => + new SmoothingPoint2D(x, y, arcLength, 0d, 0d, 1d, isGearSwitch, + isGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.LocalG2Transition); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs new file mode 100644 index 0000000..50b84d5 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs @@ -0,0 +1,559 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 对一个局部 G2 替换候选执行区域质量门和完整路径安全复核。 +internal sealed class LocalG2CandidateEvaluator +{ + private const double CurvatureRangeTolerance = 1e-6d; + private const double WindowPointToleranceMeters = 1e-10d; + private static readonly IReadOnlyList EmptyPath = + new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptySegments = + new ReadOnlyCollection(new List()); + private readonly PathGeometryAnalyzer _analyzer; + private readonly LocalG2PathSplicer _splicer; + private readonly SmoothedPathValidator _validator; + + internal LocalG2CandidateEvaluator() + : this(new PathGeometryAnalyzer(), new LocalG2PathSplicer(), new SmoothedPathValidator()) + { + } + + internal LocalG2CandidateEvaluator( + PathGeometryAnalyzer analyzer, + LocalG2PathSplicer splicer, + SmoothedPathValidator validator) + { + _analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer)); + _splicer = splicer ?? throw new ArgumentNullException(nameof(splicer)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + } + + internal LocalG2CandidateEvaluation Evaluate( + PreparedPath rawPath, + PreparedPath currentPath, + LocalG2SmoothingRegion region, + LocalG2CandidateGeometry candidate, + PathSmoothingRequest request, + LocalG2OptionsSnapshot options, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + int candidateIndex = candidate == null ? -1 : candidate.CandidateIndex; + if (!HasUsableInput(rawPath, currentPath, region, candidate, request, options)) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "局部 G2 候选评价输入无效。"); + + PreparedDirectionSegment currentSegment = currentPath.Segments[candidate.SegmentIndex]; + if (!TryExtractWindow(currentSegment, candidate.StartArcLengthMeters, candidate.EndArcLengthMeters, + out IReadOnlyList rawWindow, out string reason) || + !TryAnalyzeWindow(currentSegment, rawWindow, request.Configuration.OutputSpacingMeters, + out PathGeometryAnalysis rawAnalysis, out reason) || + !TryAnalyzeWindow(currentSegment, candidate.RegionPoints, request.Configuration.OutputSpacingMeters, + out PathGeometryAnalysis candidateAnalysis, out reason) || + !HasFiniteMetrics(rawAnalysis) || !HasFiniteMetrics(candidateAnalysis)) + { + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, + string.IsNullOrEmpty(reason) ? "局部 G2 区域几何分析失败。" : reason); + } + + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double vehicleMaximumCurvature)) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "车辆曲率约束无效。"); + if (candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter > vehicleMaximumCurvature + CurvatureRangeTolerance) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureExceeded, "局部 G2 候选超过车辆曲率上限。"); + + GetCurvatureRange(rawAnalysis.Path, out double rawMinimumCurvature, out double rawMaximumCurvature); + if (ExceedsRawCurvatureRange(candidateAnalysis.Path, rawMinimumCurvature, rawMaximumCurvature)) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureOvershoot, "局部 G2 候选超出原始区域曲率范围。"); + + double maximumDeviation = MaximumDistanceToPolyline(candidate.RegionPoints, rawWindow); + if (!NumericGuard.IsFinite(maximumDeviation)) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "局部 G2 候选偏差计算失败。"); + if (maximumDeviation > options.MaximumDeviationMeters) + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.DeviationExceeded, "局部 G2 候选偏离原始窗口过远。"); + + if (!_splicer.TryReplace(currentPath, candidate, out PreparedPath spliced, out reason) || + !_analyzer.TryAnalyze(spliced.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis fullAnalysis, out reason) || + !HasFiniteMetrics(fullAnalysis)) + { + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, + string.IsNullOrEmpty(reason) ? "局部 G2 完整路径几何分析失败。" : reason); + } + + if (!_validator.TryValidate(fullAnalysis.Path, fullAnalysis.Segments, rawPath, request.Map, request.Vehicle, + request.Configuration.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, + out double minimumClearance, out reason)) + { + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.Collision, + string.IsNullOrEmpty(reason) ? "局部 G2 完整路径安全复核失败。" : reason); + } + if (!NumericGuard.IsFinite(minimumClearance) || minimumClearance < request.Configuration.MinimumClearanceReserveMeters) + return new LocalG2CandidateEvaluation(false, PathSmoothingRegionFailureReason.InsufficientClearance, candidateIndex, + null, EmptyPath, EmptySegments, rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, rawAnalysis.CurvatureVariationCost, + candidateAnalysis.CurvatureVariationCost, maximumDeviation, minimumClearance, + candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, 0d, "局部 G2 候选净空不足。"); + + if (candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter > + rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter * + (1d - options.MinimumPeakGradientImprovementRatio)) + { + return new LocalG2CandidateEvaluation(false, PathSmoothingRegionFailureReason.InsufficientImprovement, candidateIndex, + null, EmptyPath, EmptySegments, rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, rawAnalysis.CurvatureVariationCost, + candidateAnalysis.CurvatureVariationCost, maximumDeviation, minimumClearance, + candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, 0d, "局部 G2 候选曲率导数峰值改善不足。"); + } + if (candidateAnalysis.CurvatureVariationCost > rawAnalysis.CurvatureVariationCost * + (1d + options.MaximumVariationCostRegressionRatio)) + { + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.VariationCostRegression, "局部 G2 候选曲率变化代价回退。"); + } + + if (!_analyzer.TryAnalyze(rawPath.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis rawFullAnalysis, out reason) || + !HasFiniteMetrics(rawFullAnalysis)) + { + return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, + string.IsNullOrEmpty(reason) ? "原始完整路径几何分析失败。" : reason); + } + + return new LocalG2CandidateEvaluation( + true, + PathSmoothingRegionFailureReason.None, + candidateIndex, + spliced, + safePath, + fullAnalysis.Segments, + rawAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + candidateAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + rawAnalysis.CurvatureVariationCost, + candidateAnalysis.CurvatureVariationCost, + maximumDeviation, + minimumClearance, + candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter, + Math.Abs(fullAnalysis.PathLengthMeters - rawFullAnalysis.PathLengthMeters), + "Accepted"); + } + + internal static LocalG2CandidateEvaluation SelectBest(IReadOnlyList evaluations) + { + if (evaluations == null) throw new ArgumentNullException(nameof(evaluations)); + LocalG2CandidateEvaluation best = null; + for (int index = 0; index < evaluations.Count; index++) + { + LocalG2CandidateEvaluation evaluation = evaluations[index]; + if (evaluation == null || !evaluation.Accepted) continue; + if (best == null || Compare(evaluation, best) < 0) best = evaluation; + } + return best ?? Rejected(-1, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "没有通过质量门的局部 G2 候选。"); + } + + private static int Compare(LocalG2CandidateEvaluation left, LocalG2CandidateEvaluation right) + { + int result = left.MaximumDeviationMeters.CompareTo(right.MaximumDeviationMeters); + if (result != 0) return result; + result = left.ResultPeakCurvatureDerivativePerSquareMeter.CompareTo(right.ResultPeakCurvatureDerivativePerSquareMeter); + if (result != 0) return result; + result = left.ResultCurvatureVariationCost.CompareTo(right.ResultCurvatureVariationCost); + if (result != 0) return result; + result = left.AbsolutePathLengthChangeMeters.CompareTo(right.AbsolutePathLengthChangeMeters); + return result != 0 ? result : left.CandidateIndex.CompareTo(right.CandidateIndex); + } + + private static bool HasUsableInput(PreparedPath rawPath, PreparedPath currentPath, LocalG2SmoothingRegion region, + LocalG2CandidateGeometry candidate, PathSmoothingRequest request, LocalG2OptionsSnapshot options) + { + return rawPath != null && currentPath != null && region != null && candidate != null && request != null && options != null && + request.Configuration != null && request.Map != null && request.Vehicle != null && candidate.SegmentIndex == region.SegmentIndex && + candidate.SegmentIndex >= 0 && candidate.SegmentIndex < currentPath.Segments.Count && + NumericGuard.IsPositiveFinite(request.Configuration.OutputSpacingMeters) && + NumericGuard.IsPositiveFinite(request.Configuration.MaximumCollisionCheckStepMeters) && + NumericGuard.IsFinite(request.Configuration.MinimumClearanceReserveMeters) && + request.Configuration.MinimumClearanceReserveMeters >= 0d; + } + + private static bool TryExtractWindow(PreparedDirectionSegment segment, double startArcLength, double endArcLength, + out IReadOnlyList window, out string reason) + { + window = null; + reason = string.Empty; + if (segment == null || !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, startArcLength, out SmoothingPoint2D start, out reason) || + !PathReferenceInterpolator.TryInterpolateByArcLength(segment.Points, endArcLength, out SmoothingPoint2D end, out reason)) return false; + var points = new List { start }; + for (int index = 0; index < segment.Points.Count; index++) + { + SmoothingPoint2D point = segment.Points[index]; + if (point.ArcLength > startArcLength && point.ArcLength < endArcLength && + !SamePosition(points[points.Count - 1], point)) + { + points.Add(point); + } + } + if (SamePosition(points[points.Count - 1], end)) + points[points.Count - 1] = end; + else + points.Add(end); + window = new ReadOnlyCollection(points); + return true; + } + + private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right) + { + if (left == null || right == null) return false; + double x = right.X - left.X; + double y = right.Y - left.Y; + return x * x + y * y <= WindowPointToleranceMeters * WindowPointToleranceMeters; + } + + private bool TryAnalyzeWindow(PreparedDirectionSegment source, IReadOnlyList points, double spacing, + out PathGeometryAnalysis analysis, out string reason) + { + var segment = new PreparedDirectionSegment(0, source.Direction, points, false, false, + source.Points[0].ArcLength == points[0].ArcLength ? source.StartVehicleCurvaturePerMeter : null); + return _analyzer.TryAnalyze(new[] { segment }, spacing, out analysis, out reason); + } + + private static bool HasFiniteMetrics(PathGeometryAnalysis analysis) + { + return analysis != null && NumericGuard.IsFinite(analysis.PathLengthMeters) && + NumericGuard.IsFinite(analysis.MaximumAbsoluteVehicleCurvaturePerMeter) && + NumericGuard.IsFinite(analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter) && + NumericGuard.IsFinite(analysis.CurvatureVariationCost) && analysis.Path != null && analysis.Path.Count >= 2; + } + + private static void GetCurvatureRange(IReadOnlyList path, out double minimum, out double maximum) + { + minimum = double.PositiveInfinity; + maximum = double.NegativeInfinity; + for (int index = 0; index < path.Count; index++) + { + minimum = Math.Min(minimum, path[index].VehicleCurvature); + maximum = Math.Max(maximum, path[index].VehicleCurvature); + } + } + + private static bool ExceedsRawCurvatureRange(IReadOnlyList candidate, double minimum, double maximum) + { + for (int index = 0; index < candidate.Count; index++) + { + double curvature = candidate[index].VehicleCurvature; + if (curvature < minimum - CurvatureRangeTolerance || curvature > maximum + CurvatureRangeTolerance) return true; + } + return false; + } + + private static double MaximumDistanceToPolyline(IReadOnlyList candidate, IReadOnlyList raw) + { + if (candidate == null || raw == null || candidate.Count == 0 || raw.Count < 2) return double.NaN; + double maximum = 0d; + for (int index = 0; index < candidate.Count; index++) + { + double nearest = double.PositiveInfinity; + for (int segment = 1; segment < raw.Count; segment++) + nearest = Math.Min(nearest, PointToSegmentDistance(candidate[index], raw[segment - 1], raw[segment])); + maximum = Math.Max(maximum, nearest); + } + return maximum; + } + + private static double PointToSegmentDistance(SmoothingPoint2D point, SmoothingPoint2D start, SmoothingPoint2D end) + { + double dx = end.X - start.X; + double dy = end.Y - start.Y; + double lengthSquared = dx * dx + dy * dy; + if (!NumericGuard.IsPositiveFinite(lengthSquared)) return double.NaN; + double projection = ((point.X - start.X) * dx + (point.Y - start.Y) * dy) / lengthSquared; + projection = Math.Max(0d, Math.Min(1d, projection)); + double nearestX = start.X + projection * dx; + double nearestY = start.Y + projection * dy; + double distanceX = point.X - nearestX; + double distanceY = point.Y - nearestY; + return Math.Sqrt(distanceX * distanceX + distanceY * distanceY); + } + + private static LocalG2CandidateEvaluation Rejected(int candidateIndex, PathSmoothingRegionFailureReason failureReason, string reason) + { + return new LocalG2CandidateEvaluation(false, failureReason, candidateIndex, null, EmptyPath, EmptySegments, + 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, reason); + } + + /// 反射脚本使用的窄范围确定性质量门覆盖入口。 + public static class TestHooks + { + public static EvaluationTestSnapshot Execute(string scenario) + { + LocalG2CandidateEvaluation evaluation; + switch (scenario) + { + case "TooFar": + PreparedPath tooFarPath = CreateWavyPath(1d); + evaluation = Evaluate(tooFarPath, CreateCandidate(tooFarPath, 0, 1.12d), 0d); + break; + case "Overshoot": + PreparedPath overshootPath = CreateStraightPath(1d); + evaluation = Evaluate(overshootPath, CreateCandidate(overshootPath, 0, 1.15d), 0d); + break; + case "NoOp": + PreparedPath noOpPath = CreateWavyPath(1d); + evaluation = Evaluate(noOpPath, CreateCandidateFromPath(noOpPath, 0), 0d); + break; + case "Oscillating": + PreparedPath oscillatingPath = CreateOscillationBaseline(1d); + evaluation = Evaluate(oscillatingPath, CreateOscillatingCandidate(oscillatingPath), 0d); + break; + case "LowClearance": + PreparedPath lowClearancePath = CreateWavyPath(0.27d); + evaluation = Evaluate(lowClearancePath, CreateCandidateFromPath(lowClearancePath, 0), 0.02d, false, true); + break; + case "Improved": + PreparedPath improvedPath = CreateWavyPath(1d); + evaluation = Evaluate(improvedPath, CreateCandidate(improvedPath, 0, 1d), 0d); + break; + case "SmallestDeviation": + PreparedPath smallestDeviationPath = CreateWavyPath(1d); + evaluation = Evaluate(smallestDeviationPath, CreateCandidate(smallestDeviationPath, 4, 1d), 100d); + break; + case "Best": evaluation = SelectBest(CreateWavyPath(1d)); break; + default: throw new ArgumentOutOfRangeException(nameof(scenario)); + } + return new EvaluationTestSnapshot(evaluation.Accepted ? "Accepted" : "Rejected", evaluation.FailureReason.ToString(), evaluation.CandidateIndex, + evaluation.MinimumBodyClearanceMeters, evaluation.Reason); + } + + private static LocalG2CandidateEvaluation Evaluate(PreparedPath path, LocalG2CandidateGeometry candidate, double minimumClearance, + bool useNearObstacle = false, bool useEmptyMap = false) + { + PathSmoothingConfiguration configuration = CreateConfiguration(minimumClearance); + PathSmoothingRequest request = new PathSmoothingRequest(null, null, + useEmptyMap ? CreateEmptyMap() : CreateMap(useNearObstacle), CreateVehicle(), configuration); + return new LocalG2CandidateEvaluator().Evaluate(path, path, CreateRegion(), candidate, request, + new LocalG2OptionsSnapshot(configuration), CancellationToken.None); + } + + private static LocalG2CandidateEvaluation SelectBest(PreparedPath path) + { + PathSmoothingConfiguration configuration = CreateConfiguration(0d); + PathSmoothingRequest request = new PathSmoothingRequest(null, null, CreateMap(false), CreateVehicle(), configuration); + var evaluator = new LocalG2CandidateEvaluator(); + LocalG2CandidateEvaluation smallestDeviation = evaluator.Evaluate(path, path, CreateRegion(), + CreateCandidate(path, 4, 1d), request, new LocalG2OptionsSnapshot(configuration), CancellationToken.None); + LocalG2CandidateEvaluation smootherButFarther = evaluator.Evaluate(path, path, CreateRegion(), + CreateCandidate(path, 5, 1.01d), request, new LocalG2OptionsSnapshot(configuration), CancellationToken.None); + return LocalG2CandidateEvaluator.SelectBest(new[] { smootherButFarther, smallestDeviation }); + } + + private static PreparedPath CreateWavyPath(double startX) + { + return CreatePath(new[] + { + Point(startX, 1d), Point(startX + 0.10d, 1.04d), Point(startX + 0.20d, 1.08d), + Point(startX + 0.30d, 1.05d), Point(startX + 0.40d, 0.98d), Point(startX + 0.50d, 0.92d), + Point(startX + 0.60d, 0.98d), Point(startX + 0.70d, 1.05d), Point(startX + 0.80d, 1.08d), + Point(startX + 0.90d, 1.04d), Point(startX + 1d, 1d), + }); + } + + private static PreparedPath CreateStraightPath(double startX) => CreatePath(new[] { Point(startX, 1d), Point(startX + 0.5d, 1d), Point(startX + 1d, 1d) }); + + private static PreparedPath CreateOscillationBaseline(double startX) + { + return CreatePath(new[] + { + Point(startX, 1d), Point(startX + 0.05d, 1.022d), Point(startX + 0.10d, 1d), + Point(startX + 2.03d, 1d), + }); + } + + private static PreparedPath CreatePath(IReadOnlyList points) + { + var normalized = new List(points.Count); + double arcLength = 0d; + for (int index = 0; index < points.Count; index++) + { + if (index > 0) + { + double dx = points[index].X - points[index - 1].X; + double dy = points[index].Y - points[index - 1].Y; + arcLength += Math.Sqrt(dx * dx + dy * dy); + } + normalized.Add(new SmoothingPoint2D(points[index].X, points[index].Y, arcLength, 0d, 0d, 1d, + false, SmoothedPathPointSource.LocalG2Transition)); + } + return new PreparedPath(new[] { new PreparedDirectionSegment(0, TravelDirection.Forward, normalized, false, false) }); + } + + private static LocalG2CandidateGeometry CreateCandidate(PreparedPath path, int index, double middleY) + { + IReadOnlyList source = path.Segments[0].Points; + SmoothingPoint2D start = source[0]; + SmoothingPoint2D end = source[source.Count - 1]; + return new LocalG2CandidateGeometry(index, 0, start.ArcLength, end.ArcLength, 0d, 0d, + new[] { Point(start.X, start.Y), Point((start.X + end.X) / 2d, middleY), Point(end.X, end.Y) }, + 0d, 0d, 0d, 0d, true); + } + + private static LocalG2CandidateGeometry CreateCandidateFromPath(PreparedPath path, int index) + { + IReadOnlyList points = path.Segments[0].Points; + return new LocalG2CandidateGeometry(index, 0, points[0].ArcLength, points[points.Count - 1].ArcLength, 0d, 0d, points, + 0d, 0d, 0d, 0d, true); + } + + private static LocalG2CandidateGeometry CreateOscillatingCandidate(PreparedPath path) + { + IReadOnlyList source = path.Segments[0].Points; + SmoothingPoint2D start = source[0]; + SmoothingPoint2D end = source[source.Count - 1]; + var points = new List { Point(start.X, start.Y) }; + SmoothingPoint2D straightStart = source[source.Count - 2]; + points.Add(Point(straightStart.X, straightStart.Y)); + for (int index = 1; index < 20; index++) + { + double x = straightStart.X + index * (end.X - straightStart.X) / 20d; + points.Add(Point(x, straightStart.Y + (index % 2 == 0 ? 0.004d : -0.004d))); + } + points.Add(Point(end.X, end.Y)); + return new LocalG2CandidateGeometry(0, 0, start.ArcLength, end.ArcLength, 0d, 0d, points, 0d, 0d, 0d, 0d, true); + } + + private static LocalG2SmoothingRegion CreateRegion() + { + return new LocalG2SmoothingRegion(0, + new[] { new CurvatureTransition(0, 1, 2, 0.5d, 0.5d, 0d, 0d, 0d, 0d) }, + 0d, 1d, new[] { new LocalG2WindowVariant(0, 0d, 1d, 0d, 0d) }); + } + + private static PathSmoothingConfiguration CreateConfiguration(double minimumClearance) + { + var configuration = new PathSmoothingConfiguration + { + OutputSpacingMeters = 0.05d, + MaximumCollisionCheckStepMeters = 0.05d, + MinimumClearanceReserveMeters = minimumClearance, + }; + configuration.LocalG2Quintic.MaximumDeviationMeters = 0.10d; + configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = 0.20d; + configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio = 0.02d; + return configuration; + } + + private static VehicleParameters CreateVehicle() => new VehicleParameters + { + LengthMeters = 0.20d, WidthMeters = 0.20d, SafetyMarginMeters = 0d, + MaximumCurvaturePerMeter = 1000000d, MinimumTurningRadiusMeters = 0.000001d, + }; + + private static PlanningGridMap CreateMap(bool useNearObstacle) + { + var request = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 4000f, 0f, 4000f), ResolutionMm = 20f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource("local-g2-evaluator", 1, true, new IMapObstacle[] + { + useNearObstacle + ? new AxisAlignedRectangleObstacle(100f, 150f, 900f, 1100f) + : new AxisAlignedRectangleObstacle(3000f, 3100f, 3000f, 3100f), + }), + }, + }; + PlanningMapBuildResult result = new PlanningMapFactory().Create(request); + if (!result.Succeeded || result.Map == null) throw new InvalidOperationException("测试地图创建失败。"); + return result.Map; + } + + private static PlanningGridMap CreateEmptyMap() + { + var request = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 4000f, 0f, 4000f), ResolutionMm = 20f, + ObstacleSources = Array.Empty(), AllowExplicitEmptyMap = true, + }; + PlanningMapBuildResult result = new PlanningMapFactory().Create(request); + if (!result.Succeeded || result.Map == null) throw new InvalidOperationException("测试空地图创建失败。"); + return result.Map; + } + + private static SmoothingPoint2D Point(double x, double y) => + new SmoothingPoint2D(x, y, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.LocalG2Transition); + + public sealed class EvaluationTestSnapshot + { + internal EvaluationTestSnapshot(string status, string failureReason, int candidateIndex, double minimumClearanceMeters, string reason) + { + Status = status; + FailureReason = failureReason; + CandidateIndex = candidateIndex; + MinimumClearanceMeters = minimumClearanceMeters; + Reason = reason; + } + public string Status { get; } + public string FailureReason { get; } + public int CandidateIndex { get; } + public double MinimumClearanceMeters { get; } + public string Reason { get; } + } + } +} + +/// 不可变的局部 G2 候选质量与安全评价结果。 +internal sealed class LocalG2CandidateEvaluation +{ + internal LocalG2CandidateEvaluation(bool accepted, PathSmoothingRegionFailureReason failureReason, int candidateIndex, + PreparedPath splicedPreparedPath, IReadOnlyList safePath, IReadOnlyList safeSegments, + double rawPeakCurvatureDerivativePerSquareMeter, double resultPeakCurvatureDerivativePerSquareMeter, + double rawCurvatureVariationCost, double resultCurvatureVariationCost, double maximumDeviationMeters, + double minimumBodyClearanceMeters, double maximumAbsoluteVehicleCurvaturePerMeter, + double absolutePathLengthChangeMeters, string reason) + { + Accepted = accepted; + FailureReason = failureReason; + CandidateIndex = candidateIndex; + SplicedPreparedPath = splicedPreparedPath; + SafePath = Copy(safePath); + SafeSegments = Copy(safeSegments); + RawPeakCurvatureDerivativePerSquareMeter = rawPeakCurvatureDerivativePerSquareMeter; + ResultPeakCurvatureDerivativePerSquareMeter = resultPeakCurvatureDerivativePerSquareMeter; + RawCurvatureVariationCost = rawCurvatureVariationCost; + ResultCurvatureVariationCost = resultCurvatureVariationCost; + MaximumDeviationMeters = maximumDeviationMeters; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; + AbsolutePathLengthChangeMeters = absolutePathLengthChangeMeters; + Reason = reason ?? string.Empty; + } + + internal bool Accepted { get; } + internal PathSmoothingRegionFailureReason FailureReason { get; } + internal int CandidateIndex { get; } + internal PreparedPath SplicedPreparedPath { get; } + internal IReadOnlyList SafePath { get; } + internal IReadOnlyList SafeSegments { get; } + internal double RawPeakCurvatureDerivativePerSquareMeter { get; } + internal double ResultPeakCurvatureDerivativePerSquareMeter { get; } + internal double RawCurvatureVariationCost { get; } + internal double ResultCurvatureVariationCost { get; } + internal double MaximumDeviationMeters { get; } + internal double MinimumBodyClearanceMeters { get; } + internal double MaximumAbsoluteVehicleCurvaturePerMeter { get; } + internal double AbsolutePathLengthChangeMeters { get; } + internal string Reason { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs new file mode 100644 index 0000000..22161f7 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 一个尚未经过安全和质量评价的局部 G2 替换几何。 +internal sealed class LocalG2CandidateGeometry +{ + internal LocalG2CandidateGeometry( + int candidateIndex, + int segmentIndex, + double startArcLengthMeters, + double endArcLengthMeters, + double leftWindowLengthMeters, + double rightWindowLengthMeters, + IReadOnlyList regionPoints, + double startVehicleCurvaturePerMeter, + double endVehicleCurvaturePerMeter, + double startGeometricCurvaturePerMeter, + double endGeometricCurvaturePerMeter, + bool internalConnectionsAreG2) + { + if (candidateIndex < 0 || segmentIndex < 0 || !NumericGuard.IsFinite(startArcLengthMeters) || + !NumericGuard.IsFinite(endArcLengthMeters) || startArcLengthMeters < 0d || + endArcLengthMeters < startArcLengthMeters || !NumericGuard.IsFinite(leftWindowLengthMeters) || + !NumericGuard.IsFinite(rightWindowLengthMeters) || leftWindowLengthMeters < 0d || + rightWindowLengthMeters < 0d || regionPoints == null || regionPoints.Count < 2 || + !NumericGuard.IsFinite(startVehicleCurvaturePerMeter) || !NumericGuard.IsFinite(endVehicleCurvaturePerMeter) || + !NumericGuard.IsFinite(startGeometricCurvaturePerMeter) || !NumericGuard.IsFinite(endGeometricCurvaturePerMeter)) + { + throw new ArgumentOutOfRangeException(nameof(regionPoints)); + } + + CandidateIndex = candidateIndex; + SegmentIndex = segmentIndex; + StartArcLengthMeters = startArcLengthMeters; + EndArcLengthMeters = endArcLengthMeters; + LeftWindowLengthMeters = leftWindowLengthMeters; + RightWindowLengthMeters = rightWindowLengthMeters; + RegionPoints = Copy(regionPoints); + StartVehicleCurvaturePerMeter = startVehicleCurvaturePerMeter; + EndVehicleCurvaturePerMeter = endVehicleCurvaturePerMeter; + StartGeometricCurvaturePerMeter = startGeometricCurvaturePerMeter; + EndGeometricCurvaturePerMeter = endGeometricCurvaturePerMeter; + InternalConnectionsAreG2 = internalConnectionsAreG2; + } + + internal int CandidateIndex { get; } + internal int SegmentIndex { get; } + internal double StartArcLengthMeters { get; } + internal double EndArcLengthMeters { get; } + internal double LeftWindowLengthMeters { get; } + internal double RightWindowLengthMeters { get; } + internal IReadOnlyList RegionPoints { get; } + + // 这些边界状态供候选评价和窄范围反射验证使用,不能替代统一几何分析器的最终统计。 + internal double StartVehicleCurvaturePerMeter { get; } + internal double EndVehicleCurvaturePerMeter { get; } + internal double StartGeometricCurvaturePerMeter { get; } + internal double EndGeometricCurvaturePerMeter { get; } + internal bool InternalConnectionsAreG2 { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + { + if (source[index] == null) throw new ArgumentOutOfRangeException(nameof(source)); + copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs new file mode 100644 index 0000000..3fddd10 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2OptionsSnapshot.cs @@ -0,0 +1,57 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 局部 G2 预平滑一次运行使用的已校验不可变选项。 +internal sealed class LocalG2OptionsSnapshot +{ + internal LocalG2OptionsSnapshot(PathSmoothingConfiguration configuration) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + LocalG2QuinticOptions source = configuration.LocalG2Quintic; + if (source == null) throw new ArgumentOutOfRangeException(nameof(configuration)); + + MinimumWindowLengthMeters = source.MinimumWindowLengthMeters; + PreferredWindowLengthMeters = source.PreferredWindowLengthMeters; + MaximumWindowLengthMeters = source.MaximumWindowLengthMeters; + MaximumDeviationMeters = source.MaximumDeviationMeters; + AbsoluteCurvatureJumpFloorPerMeter = source.AbsoluteCurvatureJumpFloorPerMeter; + CurvatureJumpRatioOfMaximum = source.CurvatureJumpRatioOfMaximum; + MinimumPeakGradientImprovementRatio = source.MinimumPeakGradientImprovementRatio; + MaximumVariationCostRegressionRatio = source.MaximumVariationCostRegressionRatio; + MaximumCandidatesPerRegion = source.MaximumCandidatesPerRegion; + + if (!NumericGuard.IsPositiveFinite(MinimumWindowLengthMeters)) + throw new ArgumentOutOfRangeException(nameof(MinimumWindowLengthMeters)); + if (!NumericGuard.IsFinite(PreferredWindowLengthMeters) || PreferredWindowLengthMeters < MinimumWindowLengthMeters) + throw new ArgumentOutOfRangeException(nameof(PreferredWindowLengthMeters)); + if (!NumericGuard.IsFinite(MaximumWindowLengthMeters) || MaximumWindowLengthMeters < PreferredWindowLengthMeters) + throw new ArgumentOutOfRangeException(nameof(MaximumWindowLengthMeters)); + if (!NumericGuard.IsPositiveFinite(MaximumDeviationMeters)) + throw new ArgumentOutOfRangeException(nameof(MaximumDeviationMeters)); + if (!NumericGuard.IsPositiveFinite(AbsoluteCurvatureJumpFloorPerMeter)) + throw new ArgumentOutOfRangeException(nameof(AbsoluteCurvatureJumpFloorPerMeter)); + if (!NumericGuard.IsFinite(CurvatureJumpRatioOfMaximum) || CurvatureJumpRatioOfMaximum <= 0d || CurvatureJumpRatioOfMaximum > 1d) + throw new ArgumentOutOfRangeException(nameof(CurvatureJumpRatioOfMaximum)); + if (!NumericGuard.IsFinite(MinimumPeakGradientImprovementRatio) || + MinimumPeakGradientImprovementRatio <= 0d || MinimumPeakGradientImprovementRatio >= 1d) + { + throw new ArgumentOutOfRangeException(nameof(MinimumPeakGradientImprovementRatio)); + } + if (!NumericGuard.IsFinite(MaximumVariationCostRegressionRatio) || MaximumVariationCostRegressionRatio < 0d) + throw new ArgumentOutOfRangeException(nameof(MaximumVariationCostRegressionRatio)); + if (MaximumCandidatesPerRegion < 1) + throw new ArgumentOutOfRangeException(nameof(MaximumCandidatesPerRegion)); + } + + internal double MinimumWindowLengthMeters { get; } + internal double PreferredWindowLengthMeters { get; } + internal double MaximumWindowLengthMeters { get; } + internal double MaximumDeviationMeters { get; } + internal double AbsoluteCurvatureJumpFloorPerMeter { get; } + internal double CurvatureJumpRatioOfMaximum { get; } + internal double MinimumPeakGradientImprovementRatio { get; } + internal double MaximumVariationCostRegressionRatio { get; } + internal int MaximumCandidatesPerRegion { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs new file mode 100644 index 0000000..294efe0 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 只替换一个方向段内局部窗口,并保持其余方向拓扑不变。 +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(); + 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 localPoints, out reason)) return false; + + var segments = new List(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 points, + out IReadOnlyList recalculated, + out string reason) + { + recalculated = null; + reason = string.Empty; + if (points == null || points.Count < 2) + { + reason = "局部 G2 拼接后方向段没有足够点。"; + return false; + } + var output = new List(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 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); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs new file mode 100644 index 0000000..a2b8d42 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// Publishes independently validated Local G2 regional replacements through the dedicated service route. +internal sealed class LocalG2PreSmoothingPipeline +{ + private readonly CurvatureTransitionDetector _detector = new CurvatureTransitionDetector(); + private readonly LocalG2WindowPlanner _windowPlanner = new LocalG2WindowPlanner(); + private readonly LocalG2CandidateBuilder _builder = new LocalG2CandidateBuilder(); + private readonly LocalG2CandidateEvaluator _evaluator = new LocalG2CandidateEvaluator(); + private readonly LocalG2RegionWorkOrder _workOrder = new LocalG2RegionWorkOrder(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); + + internal PathSmoothingResult Smooth( + PathSmoothingRequest request, + PreparedPath preparedPath, + RawPathBaseline rawBaseline, + CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + if (request == null || preparedPath == null || rawBaseline == null || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvature)) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, "局部 G2 预平滑输入无效。"); + } + + var options = new LocalG2OptionsSnapshot(request.Configuration); + if (!_detector.TryDetect(request, maximumCurvature, options, out IReadOnlyList transitions, out string reason) || + !_windowPlanner.TryPlan(preparedPath, transitions, options, out IReadOnlyList regions, out reason)) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, reason); + } + + IReadOnlyList reportOrder = + new ReadOnlyCollection(new List(regions)); + if (!_workOrder.TryCreate(reportOrder, out IReadOnlyList workRegions, out string orderReason)) + return Failure(PathSmoothingStatus.Failed, stopwatch, orderReason); + + PreparedPath current = preparedPath; + var reportsByRegion = new Dictionary(); + var accepted = new List(); + int improvedCount = 0; + + foreach (LocalG2SmoothingRegion region in workRegions) + { + cancellationToken.ThrowIfCancellationRequested(); + IReadOnlyList candidates = _builder.Build( + preparedPath.Segments[region.SegmentIndex], region, + request.Configuration.OutputSpacingMeters, options, cancellationToken); + var evaluations = new List(); + for (int candidateIndex = 0; candidateIndex < candidates.Count; candidateIndex++) + evaluations.Add(_evaluator.Evaluate( + preparedPath, current, region, candidates[candidateIndex], request, options, cancellationToken)); + + LocalG2CandidateEvaluation best = LocalG2CandidateEvaluator.SelectBest(evaluations); + if (best.Accepted) + { + accepted.Add(new AcceptedRegion(region, current, best)); + current = best.SplicedPreparedPath; + improvedCount++; + reportsByRegion.Add(region, CreateImprovedReport(region, candidates.Count, best)); + } + else + { + reportsByRegion.Add(region, CreateRetainedReport(region, candidates.Count, best)); + } + } + + if (!TryValidateFinal(current, preparedPath, rawBaseline, request, options, out IReadOnlyList path, + out IReadOnlyList segments, out PathQualityMetrics metrics, out reason)) + { + for (int index = accepted.Count - 1; index >= 0; index--) + { + AcceptedRegion rollback = accepted[index]; + current = rollback.Before; + reportsByRegion[rollback.Region] = CreateRollbackReport(rollback.Region, rollback.Evaluation); + improvedCount--; + if (TryValidateFinal(current, preparedPath, rawBaseline, request, options, out path, out segments, out metrics, out reason)) + break; + } + } + + if (metrics == null) + return Failure(PathSmoothingStatus.Failed, stopwatch, reason); + + var reports = new List(reportOrder.Count); + for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++) + reports.Add(reportsByRegion[reportOrder[reportIndex]]); + + PathSmoothingStatus status; + if (transitions.Count == 0) status = PathSmoothingStatus.NotNeeded; + else if (improvedCount == regions.Count) status = PathSmoothingStatus.Complete; + else if (improvedCount > 0) status = PathSmoothingStatus.PartialImprovement; + else status = PathSmoothingStatus.Unchanged; + return PathSmoothingResult.PublishLocalG2( + status, + path, + segments, + new PathSmoothingDiagnostics(metrics, stopwatch.Elapsed, 0, 0d, reason ?? string.Empty), + reports); + } + catch (OperationCanceledException) + { + return Failure(PathSmoothingStatus.Cancelled, stopwatch, "路径平滑已取消。"); + } + catch (Exception exception) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, exception.Message); + } + } + + private bool TryValidateFinal( + PreparedPath current, + PreparedPath rawPath, + RawPathBaseline rawBaseline, + PathSmoothingRequest request, + LocalG2OptionsSnapshot options, + out IReadOnlyList path, + out IReadOnlyList segments, + out PathQualityMetrics metrics, + out string reason) + { + path = null; + segments = null; + metrics = null; + reason = string.Empty; + if (!_analyzer.TryAnalyze(current.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason) || + !_validator.TryValidate(analysis.Path, analysis.Segments, rawPath, request.Map, request.Vehicle, + request.Configuration.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, + out double minimumClearance, out reason) || + minimumClearance < request.Configuration.MinimumClearanceReserveMeters || + analysis.CurvatureVariationCost > rawBaseline.Metrics.CurvatureVariationCost * + (1d + options.MaximumVariationCostRegressionRatio)) + { + if (string.IsNullOrEmpty(reason)) reason = "局部 G2 完整路径复核未通过。"; + return false; + } + + path = safePath; + segments = analysis.Segments; + metrics = new PathQualityMetrics( + true, + analysis.PathLengthMeters, + analysis.MaximumAbsoluteVehicleCurvaturePerMeter, + analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + analysis.RootMeanSquareVehicleCurvaturePerMeter, + analysis.TotalAbsoluteCurvatureVariationPerMeter, + analysis.CurvatureVariationCost, + minimumClearance, + 0d, 0d, 0d, 0d); + return true; + } + + private static PathSmoothingRegionReport CreateImprovedReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.Improved, PathSmoothingRegionFailureReason.None); + + private static PathSmoothingRegionReport CreateRetainedReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.RetainedOriginal, evaluation.FailureReason); + + private static PathSmoothingRegionReport CreateRollbackReport( + LocalG2SmoothingRegion region, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, region.WindowVariants.Count, evaluation, PathSmoothingRegionStatus.RetainedOriginal, + PathSmoothingRegionFailureReason.GlobalValidationRollback); + + private static PathSmoothingRegionReport CreateReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation, + PathSmoothingRegionStatus status, + PathSmoothingRegionFailureReason failureReason) + { + LocalG2WindowVariant window = region.WindowVariants[0]; + var jumps = new List(region.Transitions.Count); + for (int index = 0; index < region.Transitions.Count; index++) + jumps.Add(region.Transitions[index].RightVehicleCurvaturePerMeter - region.Transitions[index].LeftVehicleCurvaturePerMeter); + return new PathSmoothingRegionReport( + region.SegmentIndex, + window.StartArcLengthMeters, + window.EndArcLengthMeters, + jumps, + window.EndArcLengthMeters - window.StartArcLengthMeters, + window.EndArcLengthMeters - window.StartArcLengthMeters, + window.LeftWindowLengthMeters, + window.RightWindowLengthMeters, + candidateCount, + evaluation.CandidateIndex, + status, + failureReason, + evaluation.RawPeakCurvatureDerivativePerSquareMeter, + evaluation.ResultPeakCurvatureDerivativePerSquareMeter, + evaluation.RawCurvatureVariationCost, + evaluation.ResultCurvatureVariationCost, + evaluation.MaximumDeviationMeters, + evaluation.MinimumBodyClearanceMeters, + evaluation.MaximumAbsoluteVehicleCurvaturePerMeter); + } + + private static PathSmoothingResult Failure(PathSmoothingStatus status, Stopwatch stopwatch, string reason) => + PathSmoothingResult.Failure(status, + new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, 0, 0d, reason)); + + private sealed class AcceptedRegion + { + internal AcceptedRegion(LocalG2SmoothingRegion region, PreparedPath before, LocalG2CandidateEvaluation evaluation) + { + Region = region; + Before = before; + Evaluation = evaluation; + } + + internal LocalG2SmoothingRegion Region { get; } + internal PreparedPath Before { get; } + internal LocalG2CandidateEvaluation Evaluation { get; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs new file mode 100644 index 0000000..dde7845 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2RegionWorkOrder.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 把稳定报告顺序转换为不会使后续原始局部弧长错位的工作顺序。 +internal sealed class LocalG2RegionWorkOrder +{ + internal bool TryCreate( + IReadOnlyList reportOrder, + out IReadOnlyList workOrder, + out string reason) + { + workOrder = Empty(); + reason = string.Empty; + if (reportOrder == null) + { + reason = "局部 G2 区域工作顺序输入无效。"; + return false; + } + + var indexed = new List(reportOrder.Count); + for (int index = 0; index < reportOrder.Count; index++) + { + LocalG2SmoothingRegion region = reportOrder[index]; + if (!TryValidate(region, out double firstArc, out reason)) + return false; + indexed.Add(new IndexedRegion(region, index, firstArc)); + } + + indexed.Sort(Compare); + var ordered = new List(indexed.Count); + for (int index = 0; index < indexed.Count; index++) + ordered.Add(indexed[index].Region); + workOrder = new ReadOnlyCollection(ordered); + return true; + } + + private static bool TryValidate( + LocalG2SmoothingRegion region, + out double firstArc, + out string reason) + { + firstArc = 0d; + reason = string.Empty; + if (region == null || region.SegmentIndex < 0 || + region.Transitions == null || region.Transitions.Count == 0 || + region.WindowVariants == null || region.WindowVariants.Count == 0) + { + reason = "局部 G2 工作顺序包含空区域或空窗口集。"; + return false; + } + + double previousArc = -1d; + for (int index = 0; index < region.Transitions.Count; index++) + { + CurvatureTransition transition = region.Transitions[index]; + if (transition == null || + transition.SegmentIndex != region.SegmentIndex || + !NumericGuard.IsFinite(transition.LocalArcLengthMeters) || + transition.LocalArcLengthMeters < previousArc) + { + reason = "局部 G2 工作顺序要求区域事件有限、同段且按弧长升序。"; + return false; + } + previousArc = transition.LocalArcLengthMeters; + } + + firstArc = region.Transitions[0].LocalArcLengthMeters; + return true; + } + + private static int Compare(IndexedRegion left, IndexedRegion right) + { + int segment = left.Region.SegmentIndex.CompareTo(right.Region.SegmentIndex); + if (segment != 0) return segment; + int descendingArc = right.FirstArc.CompareTo(left.FirstArc); + return descendingArc != 0 + ? descendingArc + : left.ReportIndex.CompareTo(right.ReportIndex); + } + + private static IReadOnlyList Empty() + { + return new ReadOnlyCollection( + new List()); + } + + private sealed class IndexedRegion + { + internal IndexedRegion( + LocalG2SmoothingRegion region, + int reportIndex, + double firstArc) + { + Region = region; + ReportIndex = reportIndex; + FirstArc = firstArc; + } + + internal LocalG2SmoothingRegion Region { get; } + internal int ReportIndex { get; } + internal double FirstArc { get; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs new file mode 100644 index 0000000..e2e102a --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2SmoothingRegion.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 一个局部 G2 候选可替换的弧长窗口。 +internal sealed class LocalG2WindowVariant +{ + internal LocalG2WindowVariant(int candidateIndex, double startArcLengthMeters, double endArcLengthMeters, + double leftWindowLengthMeters, double rightWindowLengthMeters) + { + if (candidateIndex < 0 || startArcLengthMeters < 0d || endArcLengthMeters < startArcLengthMeters || + leftWindowLengthMeters < 0d || rightWindowLengthMeters < 0d) + { + throw new ArgumentOutOfRangeException(nameof(candidateIndex)); + } + CandidateIndex = candidateIndex; + StartArcLengthMeters = startArcLengthMeters; + EndArcLengthMeters = endArcLengthMeters; + LeftWindowLengthMeters = leftWindowLengthMeters; + RightWindowLengthMeters = rightWindowLengthMeters; + } + + internal int CandidateIndex { get; } + internal double StartArcLengthMeters { get; } + internal double EndArcLengthMeters { get; } + internal double LeftWindowLengthMeters { get; } + internal double RightWindowLengthMeters { get; } +} + +/// 至少存在一个联合合法生成窗口变体的一组曲率过渡。 +internal sealed class LocalG2SmoothingRegion +{ + internal LocalG2SmoothingRegion( + int segmentIndex, + IReadOnlyList transitions, + double maximumStartArcLengthMeters, + double maximumEndArcLengthMeters, + IReadOnlyList windowVariants) + { + if (segmentIndex < 0 || transitions == null || transitions.Count == 0 || + !NumericGuard.IsFinite(maximumStartArcLengthMeters) || + !NumericGuard.IsFinite(maximumEndArcLengthMeters) || + maximumStartArcLengthMeters < 0d || + maximumEndArcLengthMeters < maximumStartArcLengthMeters || + windowVariants == null || windowVariants.Count == 0) + { + throw new ArgumentOutOfRangeException(nameof(transitions)); + } + SegmentIndex = segmentIndex; + Transitions = Copy(transitions); + MaximumStartArcLengthMeters = maximumStartArcLengthMeters; + MaximumEndArcLengthMeters = maximumEndArcLengthMeters; + WindowVariants = Copy(windowVariants); + } + + internal int SegmentIndex { get; } + internal IReadOnlyList Transitions { get; } + internal double MaximumStartArcLengthMeters { get; } + internal double MaximumEndArcLengthMeters { get; } + internal IReadOnlyList WindowVariants { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs new file mode 100644 index 0000000..26143e2 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 按硬方向边界生成并合并局部 G2 曲率事件的候选窗口。 +internal sealed class LocalG2WindowPlanner +{ + private const double MergeToleranceMeters = 1e-9d; + + internal bool TryPlan( + PreparedPath originalPath, + IReadOnlyList transitions, + LocalG2OptionsSnapshot options, + out IReadOnlyList regions, + out string reason) + { + regions = Empty(); + reason = string.Empty; + if (originalPath == null || transitions == null || options == null) + { + reason = "局部 G2 窗口规划输入无效。"; + return false; + } + if (!TryGetSegmentLengths(originalPath, out Dictionary segmentLengths, out reason)) return false; + + var ordered = new List(transitions.Count); + for (int index = 0; index < transitions.Count; index++) + { + CurvatureTransition transition = transitions[index]; + if (transition == null || !segmentLengths.TryGetValue(transition.SegmentIndex, out double length) || + !NumericGuard.IsFinite(transition.LocalArcLengthMeters) || transition.LocalArcLengthMeters < 0d || + transition.LocalArcLengthMeters > length + MergeToleranceMeters) + { + reason = "局部 G2 曲率事件不属于有效方向分段。"; + return false; + } + ordered.Add(transition); + } + ordered.Sort(CompareTransitions); + + var planned = new List(); + int cursor = 0; + while (cursor < ordered.Count) + { + CurvatureTransition first = ordered[cursor]; + double segmentLength = segmentLengths[first.SegmentIndex]; + var group = new List { first }; + IReadOnlyList variants = + BuildVariants(group, segmentLength, options); + if (variants.Count == 0) + { + reason = "局部 G2 单事件无法生成满足总长度约束的窗口。"; + return false; + } + cursor++; + + while (cursor < ordered.Count && + ordered[cursor].SegmentIndex == first.SegmentIndex) + { + var tentative = new List(group) + { + ordered[cursor], + }; + IReadOnlyList tentativeVariants = + BuildVariants(tentative, segmentLength, options); + if (tentativeVariants.Count == 0) break; + group = tentative; + variants = tentativeVariants; + cursor++; + } + + double minimumStart = double.PositiveInfinity; + double maximumEnd = double.NegativeInfinity; + for (int variantIndex = 0; variantIndex < variants.Count; variantIndex++) + { + minimumStart = Math.Min( + minimumStart, + variants[variantIndex].StartArcLengthMeters); + maximumEnd = Math.Max( + maximumEnd, + variants[variantIndex].EndArcLengthMeters); + } + + planned.Add(new LocalG2SmoothingRegion( + first.SegmentIndex, + group, + minimumStart, + maximumEnd, + variants)); + } + regions = new ReadOnlyCollection(planned); + return true; + } + + private static IReadOnlyList BuildVariants( + IReadOnlyList transitions, + double segmentLength, + LocalG2OptionsSnapshot options) + { + var variants = new List(); + double firstEvent = transitions[0].LocalArcLengthMeters; + double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters; + double anchor = (firstEvent + lastEvent) / 2d; + foreach (double target in BuildTargets(options, segmentLength)) + { + if (variants.Count >= options.MaximumCandidatesPerRegion) break; + AddIfLegal(variants, target, 0.5d, anchor, firstEvent, lastEvent, segmentLength, true, options); + AddIfLegal(variants, target, 0.4d, anchor, firstEvent, lastEvent, segmentLength, false, options); + AddIfLegal(variants, target, 0.6d, anchor, firstEvent, lastEvent, segmentLength, false, options); + } + return new ReadOnlyCollection(variants); + } + + private static void AddIfLegal(List variants, double target, double leftRatio, + double anchor, double firstEvent, double lastEvent, double segmentLength, bool permitBoundaryShift, + LocalG2OptionsSnapshot options) + { + if (variants.Count >= options.MaximumCandidatesPerRegion) return; + double left = target * leftRatio; + double right = target - left; + double availableLeft = anchor; + double availableRight = segmentLength - anchor; + if (permitBoundaryShift) + { + left = Math.Min(left, availableLeft); + right = Math.Min(right, availableRight); + double missing = target - left - right; + double addRight = Math.Min(missing, availableRight - right); + right += addRight; + left += Math.Min(missing - addRight, availableLeft - left); + } + else if (left > availableLeft + MergeToleranceMeters || right > availableRight + MergeToleranceMeters) + { + return; + } + + double start = anchor - left; + double end = anchor + right; + if (start > firstEvent + MergeToleranceMeters || end + MergeToleranceMeters < lastEvent || end - start + MergeToleranceMeters < target) + return; + double actualLength = end - start; + if (actualLength + MergeToleranceMeters < options.MinimumWindowLengthMeters || + actualLength > options.MaximumWindowLengthMeters + MergeToleranceMeters) + { + return; + } + variants.Add(new LocalG2WindowVariant(variants.Count, start, end, left, right)); + } + + private static IReadOnlyList BuildTargets(LocalG2OptionsSnapshot options, double segmentLength) + { + if (segmentLength + MergeToleranceMeters < options.MinimumWindowLengthMeters) + return new ReadOnlyCollection(new List()); + + double[] requested = + { + options.PreferredWindowLengthMeters, + 0.75d * options.PreferredWindowLengthMeters, + 1.25d * options.PreferredWindowLengthMeters, + options.MinimumWindowLengthMeters, + options.MaximumWindowLengthMeters, + }; + var targets = new List(requested.Length); + for (int index = 0; index < requested.Length; index++) + { + double target = Math.Min(segmentLength, + Math.Max(options.MinimumWindowLengthMeters, Math.Min(options.MaximumWindowLengthMeters, requested[index]))); + bool duplicate = false; + for (int prior = 0; prior < targets.Count; prior++) + { + if (Math.Abs(targets[prior] - target) <= MergeToleranceMeters) + { + duplicate = true; + break; + } + } + if (!duplicate) targets.Add(target); + } + return targets; + } + + private static bool TryGetSegmentLengths(PreparedPath originalPath, out Dictionary lengths, out string reason) + { + lengths = new Dictionary(); + reason = string.Empty; + for (int position = 0; position < originalPath.Segments.Count; position++) + { + PreparedDirectionSegment segment = originalPath.Segments[position]; + if (segment == null || segment.SegmentIndex != position || segment.Points == null || segment.Points.Count == 0) + { + reason = "局部 G2 窗口规划的预处理方向分段无效。"; + return false; + } + double previousArc = -1d; + for (int pointIndex = 0; pointIndex < segment.Points.Count; pointIndex++) + { + double arc = segment.Points[pointIndex].ArcLength; + if (!NumericGuard.IsFinite(arc) || arc < 0d || arc < previousArc) + { + reason = "局部 G2 窗口规划要求分段弧长有限且非递减。"; + return false; + } + previousArc = arc; + } + lengths.Add(segment.SegmentIndex, previousArc); + } + return true; + } + + private static int CompareTransitions(CurvatureTransition left, CurvatureTransition right) + { + int segment = left.SegmentIndex.CompareTo(right.SegmentIndex); + if (segment != 0) return segment; + int arc = left.LocalArcLengthMeters.CompareTo(right.LocalArcLengthMeters); + return arc != 0 ? arc : left.LeftCoarsePathIndex.CompareTo(right.LeftCoarsePathIndex); + } + + private static IReadOnlyList Empty() => new ReadOnlyCollection(new List()); + + public static class TestHooks + { + public static WindowPlanningTestSnapshot Execute(string scenario) + { + if (string.IsNullOrWhiteSpace(scenario)) + throw new ArgumentException("A scenario is required.", nameof(scenario)); + + IReadOnlyList transitions; + double segmentLength; + switch (scenario) + { + case "SeparatedByOneMeter": + transitions = new[] + { + Transition(0.2d, 0), + Transition(1.2d, 1), + }; + segmentLength = 1.4d; + break; + case "Mergeable": + transitions = new[] + { + Transition(0.4d, 0), + Transition(0.7d, 1), + }; + segmentLength = 1.4d; + break; + case "ThreeEventPartition": + transitions = new[] + { + Transition(0.2d, 0), + Transition(0.6d, 1), + Transition(1.2d, 2), + }; + segmentLength = 1.4d; + break; + case "NearBoundary": + transitions = new[] { Transition(0.1d, 0) }; + segmentLength = 1d; + break; + default: + throw new ArgumentOutOfRangeException(nameof(scenario)); + } + + var planner = new LocalG2WindowPlanner(); + if (!planner.TryPlan( + CreatePreparedPath(segmentLength), + transitions, + new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()), + out IReadOnlyList regions, + out string reason)) + { + throw new InvalidOperationException(reason); + } + + double maximumLength = 0d; + bool exactEnvelope = true; + var counts = new List(regions.Count); + var signature = new List(); + for (int regionIndex = 0; regionIndex < regions.Count; regionIndex++) + { + LocalG2SmoothingRegion region = regions[regionIndex]; + counts.Add(region.Transitions.Count.ToString()); + double minimumStart = double.PositiveInfinity; + double maximumEnd = double.NegativeInfinity; + for (int variantIndex = 0; variantIndex < region.WindowVariants.Count; variantIndex++) + { + LocalG2WindowVariant variant = region.WindowVariants[variantIndex]; + maximumLength = Math.Max( + maximumLength, + variant.EndArcLengthMeters - variant.StartArcLengthMeters); + minimumStart = Math.Min(minimumStart, variant.StartArcLengthMeters); + maximumEnd = Math.Max(maximumEnd, variant.EndArcLengthMeters); + signature.Add( + region.SegmentIndex + ":" + + variant.CandidateIndex + ":" + + variant.StartArcLengthMeters.ToString("R") + ":" + + variant.EndArcLengthMeters.ToString("R")); + } + exactEnvelope &= Math.Abs(region.MaximumStartArcLengthMeters - minimumStart) <= 1e-9d; + exactEnvelope &= Math.Abs(region.MaximumEndArcLengthMeters - maximumEnd) <= 1e-9d; + } + + LocalG2WindowVariant first = regions[0].WindowVariants[0]; + return new WindowPlanningTestSnapshot( + regions.Count, + string.Join(",", counts), + maximumLength, + exactEnvelope, + first.LeftWindowLengthMeters, + first.RightWindowLengthMeters, + string.Join("|", signature)); + } + + public sealed class WindowPlanningTestSnapshot + { + internal WindowPlanningTestSnapshot( + int regionCount, + string transitionCounts, + double maximumWindowLength, + bool exactEnvelope, + double firstLeftLength, + double firstRightLength, + string signature) + { + RegionCount = regionCount; + TransitionCounts = transitionCounts; + MaximumWindowLength = maximumWindowLength; + ExactEnvelope = exactEnvelope; + FirstLeftLength = firstLeftLength; + FirstRightLength = firstRightLength; + Signature = signature; + } + + public int RegionCount { get; } + public string TransitionCounts { get; } + public double MaximumWindowLength { get; } + public bool ExactEnvelope { get; } + public double FirstLeftLength { get; } + public double FirstRightLength { get; } + public string Signature { get; } + } + + private static CurvatureTransition Transition(double arcLength, int index) + { + return new CurvatureTransition( + 0, + index, + index + 1, + arcLength, + arcLength, + 0d, + 0d, + index % 2 == 0 ? 0d : 0.5d, + index % 2 == 0 ? 0.5d : 0d); + } + + private static PreparedPath CreatePreparedPath(double length) + { + var points = new[] + { + new SmoothingPoint2D( + 0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothingPoint2D( + length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + }; + return new PreparedPath(new[] + { + new PreparedDirectionSegment( + 0, TravelDirection.Forward, points, false, false), + }); + } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs new file mode 100644 index 0000000..006189f --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/LocalG2/QuinticHermiteCurve2D.cs @@ -0,0 +1,124 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 满足两个二维端点二阶边界条件的参数五次 Hermite 曲线。 +internal sealed class QuinticHermiteCurve2D +{ + private readonly double _x0; + private readonly double _x1; + private readonly double _x2; + private readonly double _x3; + private readonly double _x4; + private readonly double _x5; + private readonly double _y0; + private readonly double _y1; + private readonly double _y2; + private readonly double _y3; + private readonly double _y4; + private readonly double _y5; + + private QuinticHermiteCurve2D( + double x0, double x1, double x2, double x3, double x4, double x5, + double y0, double y1, double y2, double y3, double y4, double y5) + { + _x0 = x0; + _x1 = x1; + _x2 = x2; + _x3 = x3; + _x4 = x4; + _x5 = x5; + _y0 = y0; + _y1 = y1; + _y2 = y2; + _y3 = y3; + _y4 = y4; + _y5 = y5; + } + + internal static bool TryCreate( + double x0, double y0, double dx0, double dy0, double ddx0, double ddy0, + double x1, double y1, double dx1, double dy1, double ddx1, double ddy1, + out QuinticHermiteCurve2D curve, + out string reason) + { + curve = null; + reason = string.Empty; + if (!AreFinite(x0, y0, dx0, dy0, ddx0, ddy0, x1, y1, dx1, dy1, ddx1, ddy1)) + { + reason = "五次 Hermite 曲线需要有限的边界条件。"; + return false; + } + if (IsZeroVector(dx0, dy0) || IsZeroVector(dx1, dy1)) + { + reason = "五次 Hermite 曲线端点一阶导数不能为零。"; + return false; + } + + SolveCoordinate(x0, dx0, ddx0, x1, dx1, ddx1, + out double ax0, out double ax1, out double ax2, out double ax3, out double ax4, out double ax5); + SolveCoordinate(y0, dy0, ddy0, y1, dy1, ddy1, + out double ay0, out double ay1, out double ay2, out double ay3, out double ay4, out double ay5); + if (!AreFinite(ax0, ax1, ax2, ax3, ax4, ax5, ay0, ay1, ay2, ay3, ay4, ay5)) + { + reason = "五次 Hermite 曲线系数溢出。"; + return false; + } + + curve = new QuinticHermiteCurve2D(ax0, ax1, ax2, ax3, ax4, ax5, ay0, ay1, ay2, ay3, ay4, ay5); + return true; + } + + internal void Evaluate( + double u, + out double x, out double y, + out double dx, out double dy, + out double ddx, out double ddy) + { + if (!IsFinite(u) || u < 0d || u > 1d) + throw new ArgumentOutOfRangeException(nameof(u), "The curve parameter must be finite and within [0, 1]."); + + x = EvaluateValue(_x0, _x1, _x2, _x3, _x4, _x5, u); + y = EvaluateValue(_y0, _y1, _y2, _y3, _y4, _y5, u); + dx = EvaluateFirstDerivative(_x1, _x2, _x3, _x4, _x5, u); + dy = EvaluateFirstDerivative(_y1, _y2, _y3, _y4, _y5, u); + ddx = EvaluateSecondDerivative(_x2, _x3, _x4, _x5, u); + ddy = EvaluateSecondDerivative(_y2, _y3, _y4, _y5, u); + } + + private static void SolveCoordinate(double p0, double v0, double acceleration0, double p1, double v1, double acceleration1, + out double a0, out double a1, out double a2, out double a3, out double a4, out double a5) + { + a0 = p0; + a1 = v0; + a2 = acceleration0 / 2d; + double c0 = p1 - (a0 + a1 + a2); + double c1 = v1 - (a1 + 2d * a2); + double c2 = acceleration1 - 2d * a2; + a3 = 10d * c0 - 4d * c1 + 0.5d * c2; + a4 = -15d * c0 + 7d * c1 - c2; + a5 = 6d * c0 - 3d * c1 + 0.5d * c2; + } + + private static double EvaluateValue(double a0, double a1, double a2, double a3, double a4, double a5, double u) => + (((((a5 * u + a4) * u + a3) * u + a2) * u + a1) * u) + a0; + + private static double EvaluateFirstDerivative(double a1, double a2, double a3, double a4, double a5, double u) => + ((((5d * a5 * u + 4d * a4) * u + 3d * a3) * u + 2d * a2) * u) + a1; + + private static double EvaluateSecondDerivative(double a2, double a3, double a4, double a5, double u) => + (((20d * a5 * u + 12d * a4) * u + 6d * a3) * u) + 2d * a2; + + private static bool IsZeroVector(double x, double y) => x == 0d && y == 0d; + + private static bool AreFinite(params double[] values) + { + for (int index = 0; index < values.Length; index++) + { + if (!IsFinite(values[index])) return false; + } + return true; + } + + private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs new file mode 100644 index 0000000..1205368 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/ArcLengthResampler.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 按单一方向段的弧长线性插值并保留精确锚点的确定性重采样器。 +public sealed class ArcLengthResampler +{ + private const double Tolerance = 1e-10d; + + /// 以目标间距重采样一个方向段;段末锚点始终原样保留。 + public bool TryResample( + IReadOnlyList points, + double spacingMeters, + out IReadOnlyList resampled, + out string reason) + { + resampled = EmptyPoints(); + reason = string.Empty; + if (points == null || points.Count == 0 || !NumericGuard.IsPositiveFinite(spacingMeters)) + { + reason = "重采样点集或采样间距无效。"; + return false; + } + + for (int index = 0; index < points.Count; index++) + { + if (!IsValidPoint(points[index])) + { + reason = "重采样输入包含非法数值。"; + return false; + } + if (index == 0) continue; + + SmoothingPoint2D previous = points[index - 1]; + SmoothingPoint2D current = points[index]; + if (current.ArcLength <= previous.ArcLength + Tolerance) + { + reason = "同一方向段的弧长必须严格增加。"; + return false; + } + double distance = Distance(previous, current); + if (!NumericGuard.IsFinite(distance) || distance <= Tolerance) + { + reason = "同一方向段中不允许重复位姿或数值溢出的距离。"; + return false; + } + } + + if (points.Count == 1) + { + resampled = CopyReadOnly(points); + return true; + } + + var output = new List { points[0] }; + double firstArc = points[0].ArcLength; + double finalArc = points[points.Count - 1].ArcLength; + int rightIndex = 1; + for (double targetArc = firstArc + spacingMeters; + targetArc < finalArc - Tolerance; + targetArc += spacingMeters) + { + while (rightIndex < points.Count - 1 && points[rightIndex].ArcLength < targetArc) + rightIndex++; + + SmoothingPoint2D left = points[rightIndex - 1]; + SmoothingPoint2D right = points[rightIndex]; + double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength); + double unwrappedHeading = left.UnwrappedHeading + + ratio * (right.UnwrappedHeading - left.UnwrappedHeading); + output.Add(new SmoothingPoint2D( + left.X + ratio * (right.X - left.X), + left.Y + ratio * (right.Y - left.Y), + targetArc, + AngleMath.NormalizeRadians(unwrappedHeading), + unwrappedHeading, + Math.Min(left.BodyClearance, right.BodyClearance), + false, + SmoothedPathPointSource.Interpolated)); + } + + // Appending the original object, rather than interpolating at the final arc, preserves the exact endpoint. + output.Add(points[points.Count - 1]); + resampled = new ReadOnlyCollection(output); + return true; + } + + /// 重采样一个完整方向段并保留其换向拓扑标记。 + public bool TryResample( + PreparedDirectionSegment segment, + double spacingMeters, + out PreparedDirectionSegment resampled, + out string reason) + { + resampled = null; + reason = string.Empty; + if (segment == null) + { + reason = "待重采样方向段为空。"; + return false; + } + if (!TryResample(segment.Points, spacingMeters, out IReadOnlyList points, out reason)) + return false; + + resampled = new PreparedDirectionSegment( + segment.SegmentIndex, + segment.Direction, + points, + segment.StartsAtGearSwitch, + segment.EndsAtGearSwitch, + segment.StartVehicleCurvaturePerMeter); + return true; + } + + private static bool IsValidPoint(SmoothingPoint2D point) + { + return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && + NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) && + NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) && + point.BodyClearance >= 0d; + } + + private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) + { + double x = right.X - left.X; + double y = right.Y - left.Y; + return Math.Sqrt(x * x + y * y); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList EmptyPoints() + { + return new ReadOnlyCollection(new List()); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs new file mode 100644 index 0000000..4736eca --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalysis.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 同一几何分析器产生的路径、方向段和未验证质量统计。 +public sealed class PathGeometryAnalysis +{ + internal PathGeometryAnalysis( + IReadOnlyList path, + IReadOnlyList segments, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters) + { + Path = CopyReadOnly(path); + Segments = CopyReadOnly(segments); + PathLengthMeters = pathLengthMeters; + MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; + MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter; + RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; + TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; + CurvatureVariationEnergy = curvatureVariationEnergy; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + } + + /// 完成几何重计算的不可变路径。 + public IReadOnlyList Path { get; } + + /// 完整覆盖 的不可变方向段。 + public IReadOnlyList Segments { get; } + + /// 路径总长度,单位 m。 + public double PathLengthMeters { get; } + + /// 车辆曲率绝对值峰值,单位 1/m。 + public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } + + /// 车辆曲率导数绝对值峰值,单位 1/m²。 + public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; } + + /// 车辆曲率均方根,单位 1/m。 + public double RootMeanSquareVehicleCurvaturePerMeter { get; } + + /// 不跨换向点累计的绝对曲率变化,单位 1/m。 + public double TotalAbsoluteCurvatureVariationPerMeter { get; } + + /// 不跨换向点累计的曲率变化代价。 + public double CurvatureVariationEnergy { get; } + + /// 曲率变化代价的面向用户名称;保留 以兼容既有调用方。 + public double CurvatureVariationCost => CurvatureVariationEnergy; + + /// 输入点携带的最小保守净空,单位 m。 + public double MinimumBodyClearanceMeters { get; } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs new file mode 100644 index 0000000..6153af8 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs @@ -0,0 +1,490 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 在每个单独方向段内统一重采样、恢复航向并计算几何曲率。 +public sealed class PathGeometryAnalyzer +{ + private const double MinimumDistanceMeters = 1e-10d; + private const double BoundaryToleranceMeters = 1e-8d; + + /// + /// 对候选方向段进行确定性几何分析。换向点两侧永不参与同一次差分。 + /// + public bool TryAnalyze( + IReadOnlyList candidateSegments, + double spacingMeters, + out PathGeometryAnalysis analysis, + out string reason) + { + analysis = null; + reason = string.Empty; + if (candidateSegments == null || candidateSegments.Count == 0 || + !NumericGuard.IsPositiveFinite(spacingMeters)) + { + reason = "候选方向段或输出采样间距无效。"; + return false; + } + + var outputPath = new List(); + var outputSegments = new List(); + double cumulativeArcLength = 0d; + double previousOutputHeading = 0d; + double previousOutputUnwrappedHeading = 0d; + bool hasPreviousOutputHeading = false; + double maximumAbsoluteVehicleCurvature = 0d; + double maximumAbsoluteVehicleCurvatureDerivative = 0d; + double curvatureSquareSum = 0d; + int curvatureSampleCount = 0; + double totalCurvatureVariation = 0d; + double curvatureVariationEnergy = 0d; + double minimumClearance = double.PositiveInfinity; + + for (int segmentIndex = 0; segmentIndex < candidateSegments.Count; segmentIndex++) + { + PreparedDirectionSegment segment = candidateSegments[segmentIndex]; + if (!IsValidSegment(segment, segmentIndex, out reason)) return false; + if (!TryValidateBoundary(candidateSegments, segmentIndex, out reason)) return false; + + if (!TryResampleByGeometry(segment.Points, spacingMeters, out IReadOnlyList samples, out reason)) + return false; + + if (!TryAnalyzeSegment( + segment, + samples, + ref cumulativeArcLength, + ref previousOutputHeading, + ref previousOutputUnwrappedHeading, + ref hasPreviousOutputHeading, + outputPath, + out double segmentMaximumCurvature, + out double segmentMaximumCurvatureDerivative, + out double segmentCurvatureSquareSum, + out int segmentCurvatureSampleCount, + out double segmentVariation, + out double segmentVariationEnergy, + out double segmentMinimumClearance, + out reason)) + { + return false; + } + + maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, segmentMaximumCurvature); + maximumAbsoluteVehicleCurvatureDerivative = Math.Max( + maximumAbsoluteVehicleCurvatureDerivative, + segmentMaximumCurvatureDerivative); + curvatureSquareSum += segmentCurvatureSquareSum; + curvatureSampleCount += segmentCurvatureSampleCount; + totalCurvatureVariation += segmentVariation; + curvatureVariationEnergy += segmentVariationEnergy; + minimumClearance = Math.Min(minimumClearance, segmentMinimumClearance); + int endIndex = outputPath.Count - 1; + int startIndex = endIndex - samples.Count + 1; + outputSegments.Add(new SmoothedPathSegment( + segment.SegmentIndex, + segment.Direction, + startIndex, + endIndex, + segment.StartsAtGearSwitch, + segment.EndsAtGearSwitch)); + } + + double rmsCurvature = curvatureSampleCount == 0 ? 0d : Math.Sqrt(curvatureSquareSum / curvatureSampleCount); + analysis = new PathGeometryAnalysis( + outputPath, + outputSegments, + cumulativeArcLength, + maximumAbsoluteVehicleCurvature, + maximumAbsoluteVehicleCurvatureDerivative, + rmsCurvature, + totalCurvatureVariation, + curvatureVariationEnergy, + minimumClearance); + return true; + } + + private static bool TryAnalyzeSegment( + PreparedDirectionSegment segment, + IReadOnlyList samples, + ref double cumulativeArcLength, + ref double previousOutputHeading, + ref double previousOutputUnwrappedHeading, + ref bool hasPreviousOutputHeading, + List output, + out double maximumAbsoluteVehicleCurvature, + out double maximumAbsoluteVehicleCurvatureDerivative, + out double curvatureSquareSum, + out int curvatureSampleCount, + out double totalCurvatureVariation, + out double curvatureVariationEnergy, + out double minimumClearance, + out string reason) + { + maximumAbsoluteVehicleCurvature = 0d; + maximumAbsoluteVehicleCurvatureDerivative = 0d; + curvatureSquareSum = 0d; + curvatureSampleCount = 0; + totalCurvatureVariation = 0d; + curvatureVariationEnergy = 0d; + minimumClearance = double.PositiveInfinity; + reason = string.Empty; + int count = samples.Count; + var localArcLengths = new double[count]; + var headings = new double[count]; + var unwrappedHeadings = new double[count]; + var geometricCurvatures = new double[count]; + var vehicleCurvatures = new double[count]; + var curvatureDerivatives = new double[count]; + + for (int index = 1; index < count; index++) + { + double distance = Distance(samples[index - 1], samples[index]); + if (!NumericGuard.IsFinite(distance) || distance <= MinimumDistanceMeters) + { + reason = "同一方向段中包含重复或退化的路径点。"; + return false; + } + localArcLengths[index] = localArcLengths[index - 1] + distance; + } + + for (int index = 0; index < count; index++) + { + double travelHeading; + if (index == 0 || index == count - 1) + { + // Coarse-path endpoints encode the vehicle pose at the exact integration anchor. + // A chord across a finite integration step is not that pose's heading. + travelHeading = segment.Direction == TravelDirection.Forward + ? samples[index].Heading + : samples[index].Heading - Math.PI; + } + else + { + travelHeading = Math.Atan2(samples[index + 1].Y - samples[index - 1].Y, + samples[index + 1].X - samples[index - 1].X); + } + + double heading = AngleMath.NormalizeRadians( + segment.Direction == TravelDirection.Forward ? travelHeading : travelHeading + Math.PI); + if (!NumericGuard.IsFinite(heading)) + { + reason = "候选路径航向无法归一化。"; + return false; + } + + headings[index] = heading; + if (index == 0 && !hasPreviousOutputHeading) + { + unwrappedHeadings[index] = heading; + } + else if (index == 0) + { + unwrappedHeadings[index] = previousOutputUnwrappedHeading + + AngleMath.ShortestSignedDifference(previousOutputHeading, heading); + } + else + { + unwrappedHeadings[index] = unwrappedHeadings[index - 1] + + AngleMath.ShortestSignedDifference(headings[index - 1], heading); + } + } + + for (int index = 0; index < count; index++) + { + if (count == 1) + { + geometricCurvatures[index] = 0d; + } + else + { + int leftIndex = index == 0 ? 0 : index - 1; + int rightIndex = index == count - 1 ? count - 1 : index + 1; + if (!TryEstimateGeometricCurvature( + samples, + unwrappedHeadings, + leftIndex, + rightIndex, + out geometricCurvatures[index], + out reason)) + { + return false; + } + } + + if (!NumericGuard.IsFinite(geometricCurvatures[index])) + { + reason = "候选路径曲率计算产生了非法数值。"; + return false; + } + } + + for (int index = 0; index < count; index++) + { + double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; + vehicleCurvatures[index] = directionSign * geometricCurvatures[index]; + } + + if (segment.StartVehicleCurvaturePerMeter.HasValue) + { + double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; + vehicleCurvatures[0] = segment.StartVehicleCurvaturePerMeter.Value; + geometricCurvatures[0] = directionSign * vehicleCurvatures[0]; + } + + for (int index = 0; index < count; index++) + { + if (count == 1) + { + curvatureDerivatives[index] = 0d; + } + else if (index == 0) + { + curvatureDerivatives[index] = + (vehicleCurvatures[1] - vehicleCurvatures[0]) / + (localArcLengths[1] - localArcLengths[0]); + } + else if (index == count - 1) + { + curvatureDerivatives[index] = + (vehicleCurvatures[index] - vehicleCurvatures[index - 1]) / + (localArcLengths[index] - localArcLengths[index - 1]); + } + else + { + curvatureDerivatives[index] = + (vehicleCurvatures[index + 1] - vehicleCurvatures[index - 1]) / + (localArcLengths[index + 1] - localArcLengths[index - 1]); + } + + if (!NumericGuard.IsFinite(curvatureDerivatives[index])) + { + reason = "候选路径曲率导数计算产生了非法数值。"; + return false; + } + } + + for (int index = 0; index < count; index++) + { + SmoothingPoint2D sample = samples[index]; + double vehicleCurvature = vehicleCurvatures[index]; + double arcLength = cumulativeArcLength + localArcLengths[index]; + bool isGearSwitch = index == 0 && segment.StartsAtGearSwitch; + SmoothedPathPointSource source = isGearSwitch ? SmoothedPathPointSource.GearSwitch : sample.Source; + output.Add(new SmoothedPathPoint( + sample.X, + sample.Y, + headings[index], + unwrappedHeadings[index], + arcLength, + segment.Direction, + geometricCurvatures[index], + vehicleCurvature, + curvatureDerivatives[index], + sample.BodyClearance, + isGearSwitch, + source)); + + maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, Math.Abs(vehicleCurvature)); + maximumAbsoluteVehicleCurvatureDerivative = Math.Max( + maximumAbsoluteVehicleCurvatureDerivative, + Math.Abs(curvatureDerivatives[index])); + curvatureSquareSum += vehicleCurvature * vehicleCurvature; + curvatureSampleCount++; + minimumClearance = Math.Min(minimumClearance, sample.BodyClearance); + if (index > 0) + { + double deltaCurvature = geometricCurvatures[index] - geometricCurvatures[index - 1]; + double deltaArc = localArcLengths[index] - localArcLengths[index - 1]; + totalCurvatureVariation += Math.Abs(deltaCurvature); + curvatureVariationEnergy += (deltaCurvature / deltaArc) * (deltaCurvature / deltaArc) * deltaArc; + } + } + + cumulativeArcLength += localArcLengths[count - 1]; + previousOutputHeading = headings[count - 1]; + previousOutputUnwrappedHeading = unwrappedHeadings[count - 1]; + hasPreviousOutputHeading = true; + return true; + } + + private static bool TryEstimateGeometricCurvature( + IReadOnlyList samples, + IReadOnlyList unwrappedHeadings, + int leftIndex, + int rightIndex, + out double curvature, + out string reason) + { + curvature = 0d; + reason = string.Empty; + double chordLength = Distance(samples[leftIndex], samples[rightIndex]); + if (!NumericGuard.IsFinite(chordLength) || chordLength <= 0d) + { + reason = "候选路径曲率估计弦长无效。"; + return false; + } + + double deltaHeading = unwrappedHeadings[rightIndex] - unwrappedHeadings[leftIndex]; + if (!NumericGuard.IsFinite(deltaHeading)) + { + reason = "候选路径曲率估计航向差无效。"; + return false; + } + if (Math.Abs(deltaHeading) >= Math.PI) + { + reason = "候选路径曲率估计航向差存在π歧义。"; + return false; + } + + curvature = 2d * Math.Sin(deltaHeading / 2d) / chordLength; + if (!NumericGuard.IsFinite(curvature)) + { + reason = "候选路径曲率计算产生了非法数值。"; + return false; + } + return true; + } + + private static bool TryResampleByGeometry( + IReadOnlyList input, + double spacingMeters, + out IReadOnlyList samples, + out string reason) + { + samples = null; + reason = string.Empty; + var normalized = new List(input.Count); + double localArcLength = 0d; + for (int index = 0; index < input.Count; index++) + { + SmoothingPoint2D point = input[index]; + if (!IsValidPoint(point)) + { + reason = "候选路径点包含非法数值。"; + return false; + } + + if (index > 0) + { + double distance = Distance(input[index - 1], point); + if (!NumericGuard.IsFinite(distance) || distance <= MinimumDistanceMeters) + { + reason = "同一方向段中包含重复或退化的路径点。"; + return false; + } + localArcLength += distance; + } + + normalized.Add(new SmoothingPoint2D( + point.X, point.Y, localArcLength, point.Heading, point.UnwrappedHeading, + point.BodyClearance, point.IsGearSwitchPoint, point.Source)); + } + + if (normalized.Count == 1) + { + samples = normalized; + return true; + } + + var result = new List { normalized[0] }; + double finalArc = normalized[normalized.Count - 1].ArcLength; + int rightIndex = 1; + for (double targetArc = spacingMeters; targetArc < finalArc - MinimumDistanceMeters; targetArc += spacingMeters) + { + while (rightIndex < normalized.Count - 1 && normalized[rightIndex].ArcLength < targetArc) + rightIndex++; + SmoothingPoint2D left = normalized[rightIndex - 1]; + SmoothingPoint2D right = normalized[rightIndex]; + double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength); + double unwrappedHeading = left.UnwrappedHeading + ratio * (right.UnwrappedHeading - left.UnwrappedHeading); + result.Add(new SmoothingPoint2D( + left.X + ratio * (right.X - left.X), + left.Y + ratio * (right.Y - left.Y), + targetArc, + AngleMath.NormalizeRadians(unwrappedHeading), + unwrappedHeading, + Math.Min(left.BodyClearance, right.BodyClearance), + false, + SmoothedPathPointSource.Interpolated)); + } + + result.Add(normalized[normalized.Count - 1]); + samples = result; + return true; + } + + private static bool IsValidSegment(PreparedDirectionSegment segment, int expectedIndex, out string reason) + { + reason = string.Empty; + if (segment == null || segment.SegmentIndex != expectedIndex || + (segment.Direction != TravelDirection.Forward && segment.Direction != TravelDirection.Reverse) || + segment.Points == null || segment.Points.Count == 0) + { + reason = "候选方向段索引、方向或点集无效。"; + return false; + } + return true; + } + + private static bool TryValidateBoundary( + IReadOnlyList segments, + int segmentIndex, + out string reason) + { + reason = string.Empty; + PreparedDirectionSegment current = segments[segmentIndex]; + if (segmentIndex == segments.Count - 1 && current.EndsAtGearSwitch) + { + reason = "末个方向段不得声明不存在的后续换向点。"; + return false; + } + if (segmentIndex == 0) + { + if (current.StartsAtGearSwitch) + { + reason = "首个方向段不得从换向点开始。"; + return false; + } + return true; + } + + PreparedDirectionSegment previous = segments[segmentIndex - 1]; + if (previous == null || !previous.EndsAtGearSwitch || !current.StartsAtGearSwitch || + previous.Direction == current.Direction) + { + reason = "方向段边界必须是前后成对且方向相反的换向点。"; + return false; + } + + SmoothingPoint2D previousEnd = previous.Points[previous.Points.Count - 1]; + SmoothingPoint2D currentStart = current.Points[0]; + if (!currentStart.IsGearSwitchPoint || + Math.Abs(previousEnd.X - currentStart.X) > BoundaryToleranceMeters || + Math.Abs(previousEnd.Y - currentStart.Y) > BoundaryToleranceMeters || + Math.Abs(AngleMath.ShortestSignedDifference(previousEnd.Heading, currentStart.Heading)) > BoundaryToleranceMeters) + { + reason = "换向点两侧必须保留同一位姿和航向。"; + return false; + } + return true; + } + + private static bool IsValidPoint(SmoothingPoint2D point) + { + return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && + NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && + NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.BodyClearance) && + point.BodyClearance >= 0d; + } + + private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right) + { + double deltaX = right.X - left.X; + double deltaY = right.Y - left.Y; + return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs new file mode 100644 index 0000000..f8cf80b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathReferenceInterpolator.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 按方向段局部弧长插值平滑算法的原始路径参考。 +internal static class PathReferenceInterpolator +{ + internal static bool TryInterpolateByArcLength( + IReadOnlyList points, + double targetArcLength, + out SmoothingPoint2D reference, + out string reason) + { + reference = null; + reason = string.Empty; + if (points == null || points.Count == 0 || !NumericGuard.IsFinite(targetArcLength)) + { + reason = "原始路径参考点或目标弧长无效。"; + return false; + } + + for (int index = 0; index < points.Count; index++) + { + SmoothingPoint2D point = points[index]; + if (!IsValid(point) || (index > 0 && point.ArcLength < points[index - 1].ArcLength)) + { + reason = "原始路径参考点包含非法数值或非递增弧长。"; + return false; + } + } + + SmoothingPoint2D first = points[0]; + SmoothingPoint2D last = points[points.Count - 1]; + if (targetArcLength < first.ArcLength || targetArcLength > last.ArcLength) + { + reason = "目标弧长不在原始方向段范围内。"; + return false; + } + + if (targetArcLength == first.ArcLength) + { + reference = first; + return true; + } + if (targetArcLength == last.ArcLength) + { + reference = last; + return true; + } + + for (int rightIndex = 1; rightIndex < points.Count; rightIndex++) + { + SmoothingPoint2D left = points[rightIndex - 1]; + SmoothingPoint2D right = points[rightIndex]; + if (targetArcLength > right.ArcLength) continue; + if (targetArcLength == right.ArcLength) + { + reference = right; + return true; + } + + double interval = right.ArcLength - left.ArcLength; + if (!NumericGuard.IsPositiveFinite(interval)) + { + reason = "原始路径参考点包含无法插值的重复弧长。"; + return false; + } + + double ratio = (targetArcLength - left.ArcLength) / interval; + reference = new SmoothingPoint2D( + left.X + ratio * (right.X - left.X), + left.Y + ratio * (right.Y - left.Y), + targetArcLength, + left.Heading + ratio * (right.Heading - left.Heading), + left.UnwrappedHeading + ratio * (right.UnwrappedHeading - left.UnwrappedHeading), + left.BodyClearance + ratio * (right.BodyClearance - left.BodyClearance), + false, + SmoothedPathPointSource.Interpolated); + if (!IsValid(reference)) + { + reference = null; + reason = "原始路径参考插值产生非法数值。"; + return false; + } + return true; + } + + reason = "原始路径参考无法定位目标弧长。"; + return false; + } + + private static bool IsValid(SmoothingPoint2D point) + { + return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && + NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) && + NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) && + point.ArcLength >= 0d && point.BodyClearance >= 0d; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs new file mode 100644 index 0000000..23aaa63 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PathSmoothingPreprocessor.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 校验粗路径契约、保护换向拓扑并产生统一间距的平滑输入。 +public sealed class PathSmoothingPreprocessor +{ + private const double Tolerance = 1e-8d; + private readonly ArcLengthResampler _resampler; + + /// 创建使用默认确定性重采样器的预处理器。 + public PathSmoothingPreprocessor() + : this(new ArcLengthResampler()) + { + } + + /// 创建使用指定重采样器的预处理器。 + public PathSmoothingPreprocessor(ArcLengthResampler resampler) + { + _resampler = resampler ?? throw new ArgumentNullException(nameof(resampler)); + } + + /// 将一条粗路径请求校验、按方向拆分并按配置间距重采样。 + public bool TryPrepare(PathSmoothingRequest request, out PreparedPath preparedPath, out string reason) + { + preparedPath = null; + reason = string.Empty; + if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || + request.CoarsePath == null || request.Segments == null || request.CoarsePath.Count == 0 || request.Segments.Count == 0) + { + reason = "平滑请求缺少粗路径、方向分段、地图、车辆或配置。"; + return false; + } + + PathSmoothingConfiguration configuration = request.Configuration; + if (!NumericGuard.IsPositiveFinite(configuration.OutputSpacingMeters)) + { + reason = "平滑输出采样间距无效。"; + return false; + } + + if (!ValidatePathPoints(request.CoarsePath, out reason) || + !ValidateSegments(request.CoarsePath, request.Segments, out reason)) + { + return false; + } + + var preparedSegments = new List(request.Segments.Count); + for (int segmentIndex = 0; segmentIndex < request.Segments.Count; segmentIndex++) + { + PathSegment sourceSegment = request.Segments[segmentIndex]; + double segmentStartArcLength = request.CoarsePath[sourceSegment.StartIndex].ArcLength; + var segmentPoints = new List(sourceSegment.EndIndex - sourceSegment.StartIndex + 1); + for (int pointIndex = sourceSegment.StartIndex; pointIndex <= sourceSegment.EndIndex; pointIndex++) + { + CoarsePathPoint point = request.CoarsePath[pointIndex]; + bool isGearSwitch = point.IsGearSwitchPoint; + segmentPoints.Add(new SmoothingPoint2D( + point.X, + point.Y, + point.ArcLength - segmentStartArcLength, + point.Heading, + point.UnwrappedHeading, + point.BodyClearance, + isGearSwitch, + isGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.Anchor)); + } + + var unresampled = new PreparedDirectionSegment( + sourceSegment.SegmentIndex, + sourceSegment.Direction, + segmentPoints, + sourceSegment.StartsAtGearSwitch, + sourceSegment.EndsAtGearSwitch, + request.CoarsePath[sourceSegment.StartIndex].VehicleCurvature); + if (!_resampler.TryResample(unresampled, configuration.OutputSpacingMeters, out PreparedDirectionSegment resampled, out reason)) + return false; + preparedSegments.Add(new PreparedDirectionSegment( + resampled.SegmentIndex, + resampled.Direction, + resampled.Points, + resampled.StartsAtGearSwitch, + resampled.EndsAtGearSwitch, + unresampled.StartVehicleCurvaturePerMeter)); + } + + preparedPath = new PreparedPath(preparedSegments); + return true; + } + + private static bool ValidatePathPoints(IReadOnlyList path, out string reason) + { + reason = string.Empty; + CoarsePathPoint first = path[0]; + if (!IsValidPoint(first) || first.IsGearSwitchPoint || Math.Abs(first.ArcLength) > Tolerance) + { + reason = "粗路径首点无效。"; + return false; + } + + for (int index = 1; index < path.Count; index++) + { + CoarsePathPoint previous = path[index - 1]; + CoarsePathPoint current = path[index]; + if (!IsValidPoint(current) || current.ArcLength + Tolerance < previous.ArcLength) + { + reason = "粗路径包含非法数值或非递增弧长。"; + return false; + } + + double expectedHeadingDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + if (!NumericGuard.IsFinite(expectedHeadingDelta) || + Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedHeadingDelta) > Tolerance) + { + reason = "粗路径展开航向不连续。"; + return false; + } + + bool duplicatePoseAndArc = Math.Abs(current.X - previous.X) <= Tolerance && + Math.Abs(current.Y - previous.Y) <= Tolerance && + Math.Abs(current.ArcLength - previous.ArcLength) <= Tolerance && + Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= Tolerance; + if (duplicatePoseAndArc) + { + if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) + { + reason = "粗路径包含非法的重复点。"; + return false; + } + } + else if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + Tolerance) + { + reason = "粗路径普通点必须有正弧长增量且不得标记为换向点。"; + return false; + } + } + return true; + } + + private static bool ValidateSegments( + IReadOnlyList path, + IReadOnlyList segments, + out string reason) + { + reason = string.Empty; + int expectedStartIndex = 0; + for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) + { + PathSegment segment = segments[segmentIndex]; + if (segment == null || segment.SegmentIndex != segmentIndex || segment.StartIndex != expectedStartIndex || + segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count || + segment.StartsAtGearSwitch != path[segment.StartIndex].IsGearSwitchPoint) + { + reason = "粗路径方向分段索引或起始换向标记无效。"; + return false; + } + + for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) + { + if (path[pointIndex].Direction != segment.Direction) + { + reason = "粗路径方向分段包含不同方向的点。"; + return false; + } + } + + bool hasNextSegment = segmentIndex + 1 < segments.Count; + bool expectedEndsAtGearSwitch = hasNextSegment && segment.EndIndex + 1 < path.Count && + path[segment.EndIndex + 1].IsGearSwitchPoint; + if (segment.EndsAtGearSwitch != expectedEndsAtGearSwitch) + { + reason = "粗路径方向分段末尾换向标记无效。"; + return false; + } + expectedStartIndex = segment.EndIndex + 1; + } + + if (expectedStartIndex != path.Count) + { + reason = "粗路径方向分段未完整覆盖全部点。"; + return false; + } + return true; + } + + private static bool IsValidPoint(CoarsePathPoint point) + { + return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && + NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && + NumericGuard.IsFinite(point.ArcLength) && point.ArcLength >= 0d && + NumericGuard.IsFinite(point.VehicleCurvature) && NumericGuard.IsFinite(point.BodyClearance) && + point.BodyClearance >= 0d && + (point.Direction == TravelDirection.Forward || point.Direction == TravelDirection.Reverse) && + Enum.IsDefined(typeof(CoarsePathPointSource), point.Source) && + Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= Tolerance; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs new file mode 100644 index 0000000..0f58727 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedDirectionSegment.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 已校验、已按单一行驶方向分割并重采样的路径段。 +public sealed class PreparedDirectionSegment +{ + /// 创建不可变方向段。 + public PreparedDirectionSegment( + int segmentIndex, + TravelDirection direction, + IReadOnlyList points, + bool startsAtGearSwitch, + bool endsAtGearSwitch) + : this(segmentIndex, direction, points, startsAtGearSwitch, endsAtGearSwitch, null) + { + } + + /// 创建带有真实起始车辆曲率边界状态的不可变方向段。 + public PreparedDirectionSegment( + int segmentIndex, + TravelDirection direction, + IReadOnlyList points, + bool startsAtGearSwitch, + bool endsAtGearSwitch, + double? startVehicleCurvaturePerMeter) + { + if (segmentIndex < 0) throw new ArgumentOutOfRangeException(nameof(segmentIndex)); + if (points == null || points.Count == 0) throw new ArgumentException("A prepared segment requires points.", nameof(points)); + if (startVehicleCurvaturePerMeter.HasValue && !IsFinite(startVehicleCurvaturePerMeter.Value)) + throw new ArgumentOutOfRangeException(nameof(startVehicleCurvaturePerMeter)); + + SegmentIndex = segmentIndex; + Direction = direction; + Points = CopyReadOnly(points); + StartsAtGearSwitch = startsAtGearSwitch; + EndsAtGearSwitch = endsAtGearSwitch; + StartVehicleCurvaturePerMeter = startVehicleCurvaturePerMeter; + } + + /// 从零开始的分段序号;在 中必须与其位置一致。 + public int SegmentIndex { get; } + + /// 该段的唯一行驶方向。 + public TravelDirection Direction { get; } + + /// 不包含相邻段点的本段不可变采样点。 + public IReadOnlyList Points { get; } + + /// 本段首点是否为换向后保留的新方向点。 + public bool StartsAtGearSwitch { get; } + + /// 本段末点之后是否紧邻换向点。 + public bool EndsAtGearSwitch { get; } + + /// 原始车辆在本段物理起点的曲率边界状态,单位 1/m。 + public double? StartVehicleCurvaturePerMeter { get; } + + private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs new file mode 100644 index 0000000..379c33c --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/PreparedPath.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 已校验并按方向拆分的粗路径输入快照。 +public sealed class PreparedPath +{ + /// 创建不可变预处理路径。 + public PreparedPath(IReadOnlyList segments) + { + if (segments == null || segments.Count == 0) + throw new ArgumentException("A prepared path requires direction segments.", nameof(segments)); + for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) + { + if (segments[segmentIndex] == null) + throw new ArgumentException("A prepared path cannot contain null direction segments.", nameof(segments)); + } + + Segments = CopyReadOnly(segments); + Points = Flatten(Segments); + } + + /// 按原始前进/倒车拓扑排列的方向段。 + public IReadOnlyList Segments { get; } + + /// 将所有方向段顺序拼接后的点;换向重复点保留两次。 + public IReadOnlyList Points { get; } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList Flatten(IReadOnlyList segments) + { + var points = new List(); + for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) + { + PreparedDirectionSegment segment = segments[segmentIndex]; + for (int pointIndex = 0; pointIndex < segment.Points.Count; pointIndex++) + points.Add(segment.Points[pointIndex]); + } + return new ReadOnlyCollection(points); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs new file mode 100644 index 0000000..b9d34f8 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 通过候选路径同一几何分析器和验证器构建安全、可公平比较的原始基线。 +internal static class RawPathBaselineBuilder +{ + internal static bool TryCreate( + PathSmoothingRequest request, + PreparedPath preparedPath, + PathGeometryAnalyzer analyzer, + double outputSpacingMeters, + SmoothedPathValidator validator, + double maximumCollisionCheckStepMeters, + out RawPathBaseline baseline, + out string reason) + { + baseline = null; + reason = string.Empty; + if (request == null || preparedPath == null || analyzer == null || validator == null) + { + reason = "原始粗路径基线缺少请求、预处理路径、几何分析器或安全验证器。"; + return false; + } + + if (!analyzer.TryAnalyze(preparedPath.Segments, outputSpacingMeters, out PathGeometryAnalysis analysis, out reason)) + return false; + + if (validator.TryValidate( + analysis.Path, + analysis.Segments, + preparedPath, + request.Map, + request.Vehicle, + maximumCollisionCheckStepMeters, + out IReadOnlyList safePath, + out double minimumClearanceMeters, + out reason)) + { + baseline = new RawPathBaseline(safePath, analysis.Segments, CreateMetrics(analysis, minimumClearanceMeters)); + return true; + } + + if (reason != "平滑路径包含非法数值或超限车辆曲率。" || + !HasTrustedVehicleCurvaturesWithinLimit(request) || + !TryCreateTrustedRawAnalysis(request, out analysis, out reason) || + !validator.TryValidate( + analysis.Path, + analysis.Segments, + preparedPath, + request.Map, + request.Vehicle, + maximumCollisionCheckStepMeters, + out safePath, + out minimumClearanceMeters, + out reason)) + { + return false; + } + + baseline = new RawPathBaseline(safePath, analysis.Segments, CreateMetrics(analysis, minimumClearanceMeters)); + return true; + } + + private static bool HasTrustedVehicleCurvaturesWithinLimit(PathSmoothingRequest request) + { + if (request?.CoarsePath == null || request.CoarsePath.Count == 0 || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter( + request.Vehicle, + out double maximumCurvaturePerMeter)) + { + return false; + } + + for (int index = 0; index < request.CoarsePath.Count; index++) + { + CoarsePathPoint point = request.CoarsePath[index]; + if (point == null || !IsFinite(point.VehicleCurvature) || + Math.Abs(point.VehicleCurvature) > maximumCurvaturePerMeter) + { + return false; + } + } + return true; + } + + private static bool TryCreateTrustedRawAnalysis( + PathSmoothingRequest request, + out PathGeometryAnalysis analysis, + out string reason) + { + analysis = null; + reason = string.Empty; + if (request.CoarsePath == null || request.Segments == null || + request.CoarsePath.Count == 0 || request.Segments.Count == 0) + { + reason = "原始粗路径基线缺少可信路径或方向分段。"; + return false; + } + + var path = new List(request.CoarsePath.Count); + var segments = new List(request.Segments.Count); + double maximumAbsoluteVehicleCurvature = 0d; + double maximumAbsoluteVehicleCurvatureDerivative = 0d; + double curvatureSquareSum = 0d; + int curvatureSampleCount = 0; + double totalCurvatureVariation = 0d; + double curvatureVariationEnergy = 0d; + double minimumClearance = double.PositiveInfinity; + + for (int segmentIndex = 0; segmentIndex < request.Segments.Count; segmentIndex++) + { + PathSegment segment = request.Segments[segmentIndex]; + if (segment == null || segment.SegmentIndex != segmentIndex || + segment.StartIndex != path.Count || segment.EndIndex < segment.StartIndex || + segment.EndIndex >= request.CoarsePath.Count) + { + reason = "原始粗路径基线方向分段无效。"; + return false; + } + + for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) + { + CoarsePathPoint point = request.CoarsePath[pointIndex]; + if (point == null || point.Direction != segment.Direction || + !IsFinite(point.X) || !IsFinite(point.Y) || !IsFinite(point.Heading) || + !IsFinite(point.UnwrappedHeading) || !IsFinite(point.ArcLength) || point.ArcLength < 0d || + !IsFinite(point.VehicleCurvature) || !IsFinite(point.BodyClearance) || point.BodyClearance < 0d) + { + reason = "原始粗路径基线包含非法可信点。"; + return false; + } + + double derivative = EstimateVehicleCurvatureDerivative(request.CoarsePath, segment, pointIndex, out bool validDerivative); + if (!validDerivative) + { + reason = "原始粗路径基线曲率导数弧长无效。"; + return false; + } + + double directionSign = segment.Direction == TravelDirection.Forward ? 1d : -1d; + double geometricCurvature = directionSign * point.VehicleCurvature; + SmoothedPathPointSource source = point.IsGearSwitchPoint + ? SmoothedPathPointSource.GearSwitch + : SmoothedPathPointSource.CoarsePathFallback; + path.Add(new SmoothedPathPoint( + point.X, + point.Y, + point.Heading, + point.UnwrappedHeading, + point.ArcLength, + point.Direction, + geometricCurvature, + point.VehicleCurvature, + derivative, + point.BodyClearance, + point.IsGearSwitchPoint, + source)); + + maximumAbsoluteVehicleCurvature = Math.Max(maximumAbsoluteVehicleCurvature, Math.Abs(point.VehicleCurvature)); + maximumAbsoluteVehicleCurvatureDerivative = Math.Max( + maximumAbsoluteVehicleCurvatureDerivative, + Math.Abs(derivative)); + curvatureSquareSum += point.VehicleCurvature * point.VehicleCurvature; + curvatureSampleCount++; + minimumClearance = Math.Min(minimumClearance, point.BodyClearance); + if (pointIndex > segment.StartIndex) + { + CoarsePathPoint previous = request.CoarsePath[pointIndex - 1]; + double deltaArc = point.ArcLength - previous.ArcLength; + double deltaCurvature = geometricCurvature - + (directionSign * previous.VehicleCurvature); + totalCurvatureVariation += Math.Abs(deltaCurvature); + curvatureVariationEnergy += + (deltaCurvature / deltaArc) * (deltaCurvature / deltaArc) * deltaArc; + } + } + + segments.Add(new SmoothedPathSegment( + segment.SegmentIndex, + segment.Direction, + segment.StartIndex, + segment.EndIndex, + segment.StartsAtGearSwitch, + segment.EndsAtGearSwitch)); + } + + double rmsCurvature = curvatureSampleCount == 0 ? 0d : Math.Sqrt(curvatureSquareSum / curvatureSampleCount); + analysis = new PathGeometryAnalysis( + path, + segments, + request.CoarsePath[request.CoarsePath.Count - 1].ArcLength, + maximumAbsoluteVehicleCurvature, + maximumAbsoluteVehicleCurvatureDerivative, + rmsCurvature, + totalCurvatureVariation, + curvatureVariationEnergy, + minimumClearance); + return true; + } + + private static double EstimateVehicleCurvatureDerivative( + IReadOnlyList path, + PathSegment segment, + int pointIndex, + out bool valid) + { + valid = true; + if (segment.StartIndex == segment.EndIndex) return 0d; + + int leftIndex = pointIndex == segment.StartIndex ? pointIndex : pointIndex - 1; + int rightIndex = pointIndex == segment.EndIndex ? pointIndex : pointIndex + 1; + double deltaArc = path[rightIndex].ArcLength - path[leftIndex].ArcLength; + if (!IsFinite(deltaArc) || deltaArc <= 0d) + { + valid = false; + return 0d; + } + return (path[rightIndex].VehicleCurvature - path[leftIndex].VehicleCurvature) / deltaArc; + } + + private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); + + private static PathQualityMetrics CreateMetrics( + PathGeometryAnalysis analysis, + double minimumClearanceMeters) + { + return new PathQualityMetrics( + true, + analysis.PathLengthMeters, + analysis.MaximumAbsoluteVehicleCurvaturePerMeter, + analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + analysis.RootMeanSquareVehicleCurvaturePerMeter, + analysis.TotalAbsoluteCurvatureVariationPerMeter, + analysis.CurvatureVariationEnergy, + minimumClearanceMeters, + 0d, + 0d, + 0d, + 0d); + } +} + +/// 已通过完整车体复核的原始粗路径及其比较指标。 +internal sealed class RawPathBaseline +{ + internal RawPathBaseline( + IReadOnlyList path, + IReadOnlyList segments, + PathQualityMetrics metrics) + { + Path = path; + Segments = segments; + Metrics = metrics; + } + + internal IReadOnlyList Path { get; } + internal IReadOnlyList Segments { get; } + internal PathQualityMetrics Metrics { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs new file mode 100644 index 0000000..1ca0e05 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Processing/SmoothingPoint2D.cs @@ -0,0 +1,53 @@ +using System; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; + +/// 供平滑算法处理的二维路径采样点;所有长度单位均为 m,航向单位为 rad。 +public sealed class SmoothingPoint2D +{ + /// 创建不可变二维路径点。 + public SmoothingPoint2D( + double xMeters, + double yMeters, + double arcLengthMeters, + double headingRadians, + double unwrappedHeadingRadians, + double bodyClearanceMeters, + bool isGearSwitchPoint, + SmoothedPathPointSource source) + { + X = xMeters; + Y = yMeters; + ArcLength = arcLengthMeters; + Heading = headingRadians; + UnwrappedHeading = unwrappedHeadingRadians; + BodyClearance = bodyClearanceMeters; + IsGearSwitchPoint = isGearSwitchPoint; + Source = source; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 本方向段中的累计弧长,单位 m。 + public double ArcLength { get; } + + /// 归一化的车辆航向,单位 rad。 + public double Heading { get; } + + /// 连续展开的车辆航向,单位 rad。 + public double UnwrappedHeading { get; } + + /// 输入路径携带的保守净空,单位 m。 + public double BodyClearance { get; } + + /// 该点是否为新方向段开始处的换向点。 + public bool IsGearSwitchPoint { get; } + + /// 该点在平滑流程中的来源。 + public SmoothedPathPointSource Source { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md new file mode 100644 index 0000000..80d2640 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/README.md @@ -0,0 +1,56 @@ +# Path smoothing comparison + +This module compares the clamped cubic B-spline, local cubic Bézier, and piecewise-quintic smoothers against the raw Hybrid A* coarse path. It is an offline developer analysis tool; it does not alter coarse-path search acceptance. + +## Units and coordinates + +All path positions, lengths, clearances, and collision-check spacing use **meters**. Heading uses **radians** and vehicle curvature uses `1/m`. Map construction retains its existing millimeter input contract; `PlanningGridMap` provides the meter-coordinate world queries used by smoothing. A smoothing result preserves the coarse path's first and last pose, rather than the original requested goal pose, so its endpoint error is inherited from the coarse planner's accepted goal tolerance. + +## Facade usage + +Only smooth a successful coarse result. A minimal formal call flow is: + +```csharp +if (coarseResult.PlanningResult.Status != PlanningStatus.Success) + return; +var smoothing = new PathSmoothingService().Smooth( + new PathSmoothingRequest( + coarseResult.PlanningResult.Path, + coarseResult.PlanningResult.Segments, + coarseResult.MapResult.Map, + job.Vehicle, + smoothingConfiguration), + cancellationToken); +if (smoothing.Status == PathSmoothingStatus.Success || + smoothing.Status == PathSmoothingStatus.FallbackToCoarsePath) + ConsumeSpatialReference(smoothing.Path, smoothing.Segments); +``` + +## Status handling and fallback + +`Success` supplies a validated smoothed path. `FallbackToCoarsePath` is an explicit, safe degraded result and may be consumed by the same downstream spatial-reference interface. `Infeasible`, `InvalidInput`, `Failed`, and `Cancelled` must not be treated as a path. Comparison reports keep an infeasible candidate's markers for diagnosis but never select it as a recommendation. + +## Fixture freshness + +The eight fast fixtures are snapshots of successful coarse paths. Their configuration fingerprint is checked before use. After deliberately changing a scenario or planning configuration, regenerate them with `generate_path_smoothing_fixtures.ps1 -Overwrite`, then run `verify_path_smoothing_fixtures.ps1`. Fixture-only comparison never runs Hybrid A*. + +## IEEE colors, fonts, and Windows PNG + +SVG and PNG use the shared IEEE-style colors, status-bearing legends, coordinate ticks, and units. Every raw or smoothed trajectory is rendered as its complete set of discrete samples: reports intentionally draw no line segment between adjacent samples. The Windows PNG renderer requires the exact `SimSun` and `Times New Roman` font families and writes a 600 dpi raster image. If either font is unavailable, export returns `FontUnavailable`; it does not substitute a different font. PNG rendering relies on Windows GDI+, while SVG and CSV remain available without it. SVG uses text-family references, so portable publication requires checking the target font installation or converting text to paths in an external publishing tool. + +## SQP boundary + +This module is geometric smoothing and full-body validation, not sequential quadratic programming (SQP) trajectory optimization. It has no time parameterization, velocity, acceleration, steering-rate, or dynamic-obstacle constraints. Feed only its validated spatial reference into any later SQP or time-parameterization stage. + +## Output files + +`run_path_smoothing_comparison.ps1` writes developer reports only below `ClumsyPilot/obj/path_smoothing_reports`. Each scenario directory contains one `comparison.csv` and both SVG and 600 dpi PNG versions of these six focused figures: + +1. `01-coarse-path-overview` — the Hybrid A* coarse-path planning view with map, start, and goal. +2. `02-all-paths-comparison` — raw and all three smoother point clouds only, without map or endpoint decorations. +3. `03-cubic-bspline-overview` — faded coarse reference and cubic B-spline result with map context. +4. `04-local-cubic-bezier-overview` — faded coarse reference and local cubic Bézier result with map context. +5. `05-piecewise-quintic-overview` — faded coarse reference and piecewise-quintic result with map context. +6. `06-curvature-comparison` — all available curvature samples with `s (m)` and `κ (m⁻¹)` axes. + +Overhead figures preserve equal X/Y scale and use trajectory-focused bounds; their coordinate ticks are in metres. Inspect the SVG/PNG visually, retain CSV for numerical review, and use an external PDF/EPS publishing step if the final venue requires those formats. diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json new file mode 100644 index 0000000..cbd08f2 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json @@ -0,0 +1,19451 @@ +{ + "schemaVersion": 1, + "scenarios": [ + { + "id": "straight", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:d42ce997e1e51d421fe923c5819a332706007d900cc74a39d4b0ac88c6ac4a21", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.025, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.024999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.049999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.074999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.099999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.12499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.14999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1749999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.17499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1999999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.19999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.2249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2499999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.24999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.274999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.274999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2999999999999989, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.29999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3249999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.32499999999999885, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3499999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.34999999999999876, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3749999999999987, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.37499999999999867, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3999999999999986, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.39999999999999858, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4249999999999985, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.42499999999999849, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4499999999999984, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.4499999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4749999999999983, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.47499999999999831, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4999999999999982, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.49999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5249999999999981, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.52499999999999813, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.549999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.549999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.574999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.574999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5999999999999979, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.59999999999999787, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6249999999999978, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.62499999999999778, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6499999999999977, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.64999999999999769, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6749999999999976, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.6749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6999999999999975, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.69999999999999751, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7249999999999974, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.72499999999999742, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7499999999999973, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.74999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7749999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.77499999999999725, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7999999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.79999999999999716, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8249999999999971, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.82499999999999707, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.849999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.849999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8749999999999969, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.87499999999999689, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8999999999999968, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.8999999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9249999999999967, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.92499999999999671, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9499999999999966, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.94999999999999662, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9749999999999965, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.97499999999999654, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9999999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.99999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0249999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0249999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0499999999999963, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0749999999999962, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0749999999999962, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0999999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0999999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.124999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.124999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1499999999999959, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1499999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1749999999999958, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1749999999999958, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1999999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1999999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2249999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2249999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2499999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2749999999999955, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2749999999999955, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2999999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2999999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3249999999999953, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3249999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3499999999999952, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3499999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3749999999999951, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3749999999999951, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4249999999999949, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4249999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4499999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4749999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4749999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4999999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5249999999999946, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5249999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5499999999999945, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5499999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5749999999999944, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5749999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5999999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5999999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6249999999999942, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6249999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6499999999999941, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6499999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.674999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.674999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.699999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.699999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7249999999999939, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7249999999999939, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7499999999999938, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7749999999999937, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7749999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8249999999999935, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8249999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8499999999999934, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8499999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8749999999999933, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8749999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8999999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9249999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9249999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9499999999999931, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9499999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.974999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.974999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9999999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0249999999999928, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0249999999999928, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0499999999999927, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0499999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0749999999999926, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0749999999999926, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0999999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0999999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1499999999999924, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1499999999999924, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1749999999999923, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1749999999999923, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2249999999999921, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2249999999999921, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2749999999999919, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2749999999999919, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2999999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2999999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3249999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3249999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3499999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3499999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3749999999999916, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3749999999999916, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3999999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3999999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4249999999999914, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4249999999999914, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4499999999999913, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4499999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4749999999999912, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4749999999999912, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4999999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.524999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.524999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5499999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5499999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5749999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5749999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6249999999999907, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6249999999999907, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6499999999999906, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6499999999999906, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6749999999999905, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6749999999999905, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6999999999999904, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6999999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7249999999999903, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7249999999999903, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7499999999999902, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7499999999999902, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.77499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.77499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.79999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.79999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.82499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.82499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.84999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.84999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.87499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.87499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8999999999999897, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.8999999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9249999999999896, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9249999999999896, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9499999999999895, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9499999999999895, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9749999999999894, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9749999999999894, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9999999999999893, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.02499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0249999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.04999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.04999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.07499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0749999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1249999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1249999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1499999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1499999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1749999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1749999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2499999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2499999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2749999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2749999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.324999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.324999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3499999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3499999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3749999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3749999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4249999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4249999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4499999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4499999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4749999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4749999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4999999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4999999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5249999999999959, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5249999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5499999999999963, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5749999999999966, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5749999999999966, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.599999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.599999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6249999999999973, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6249999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6499999999999977, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6499999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.674999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.674999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6999999999999984, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7249999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7499999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7749999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7749999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.825, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.825, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8500000000000005, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8500000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 154, + "startsAtGearSwitch": false, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "single-turn", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:49571e2eeb2e58f0a5dc0a56e61e530700c3da7d60e2a9c66809721963e5d9f8", + "map": { + "xMinMm": -7000.0, + "xMaxMm": 11000.0, + "yMinMm": -7000.0, + "yMaxMm": 10000.0, + "resolutionMm": 50.0, + "obstacles": [] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 1.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.0249995478901843, + "yMeters": 1.0001302071559617, + "headingRadians": 0.010416666666666666, + "unwrappedHeadingRadians": 0.010416666666666666, + "arcLengthMeters": 0.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499963831803427, + "yMeters": 1.0005208144955946, + "headingRadians": 0.020833333333333332, + "unwrappedHeadingRadians": 0.020833333333333332, + "arcLengthMeters": 0.05, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749877935647825, + "yMeters": 1.0011717796356725, + "headingRadians": 0.03125, + "unwrappedHeadingRadians": 0.03125, + "arcLengthMeters": 0.075000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999710673264458, + "yMeters": 1.0020830319425964, + "headingRadians": 0.041666666666666664, + "unwrappedHeadingRadians": 0.041666666666666664, + "arcLengthMeters": 0.1, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249434936311464, + "yMeters": 1.0032544725400563, + "headingRadians": 0.052083333333333329, + "unwrappedHeadingRadians": 0.052083333333333329, + "arcLengthMeters": 0.125, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499023628217124, + "yMeters": 1.004685974319762, + "headingRadians": 0.062499999999999993, + "unwrappedHeadingRadians": 0.062499999999999993, + "arcLengthMeters": 0.15, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1748449667119993, + "yMeters": 1.0063773819552337, + "headingRadians": 0.072916666666666657, + "unwrappedHeadingRadians": 0.072916666666666657, + "arcLengthMeters": 0.175, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1997685988807447, + "yMeters": 1.0083285119186571, + "headingRadians": 0.083333333333333329, + "unwrappedHeadingRadians": 0.083333333333333329, + "arcLengthMeters": 0.2, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.224670554965231, + "yMeters": 1.0105391525007958, + "headingRadians": 0.09375, + "unwrappedHeadingRadians": 0.09375, + "arcLengthMeters": 0.22500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2495481329547253, + "yMeters": 1.0130090638339635, + "headingRadians": 0.10416666666666667, + "unwrappedHeadingRadians": 0.10416666666666667, + "arcLengthMeters": 0.25000000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2743986334836632, + "yMeters": 1.0157379779180522, + "headingRadians": 0.11458333333333334, + "unwrappedHeadingRadians": 0.11458333333333334, + "arcLengthMeters": 0.27500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2992193601245465, + "yMeters": 1.0187255986496104, + "headingRadians": 0.125, + "unwrappedHeadingRadians": 0.125, + "arcLengthMeters": 0.30000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3240076196805208, + "yMeters": 1.0219716018539722, + "headingRadians": 0.13541666666666666, + "unwrappedHeadingRadians": 0.13541666666666666, + "arcLengthMeters": 0.325, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3487607224776048, + "yMeters": 1.0254756353204335, + "headingRadians": 0.14583333333333331, + "unwrappedHeadingRadians": 0.14583333333333331, + "arcLengthMeters": 0.35, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3734759826565346, + "yMeters": 1.0292373188404673, + "headingRadians": 0.15624999999999997, + "unwrappedHeadingRadians": 0.15624999999999997, + "arcLengthMeters": 0.37499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3981507184641961, + "yMeters": 1.0332562442489797, + "headingRadians": 0.16666666666666663, + "unwrappedHeadingRadians": 0.16666666666666663, + "arcLengthMeters": 0.39999999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4227822525446114, + "yMeters": 1.0375319754685981, + "headingRadians": 0.17708333333333329, + "unwrappedHeadingRadians": 0.17708333333333329, + "arcLengthMeters": 0.42499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4473679122294476, + "yMeters": 1.0420640485569872, + "headingRadians": 0.18749999999999994, + "unwrappedHeadingRadians": 0.18749999999999994, + "arcLengthMeters": 0.44999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4719050298280172, + "yMeters": 1.0468519717571914, + "headingRadians": 0.1979166666666666, + "unwrappedHeadingRadians": 0.1979166666666666, + "arcLengthMeters": 0.47499999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963909429167384, + "yMeters": 1.051895225550991, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.49999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5208229946280243, + "yMeters": 1.0571932627152754, + "headingRadians": 0.21874999999999986, + "unwrappedHeadingRadians": 0.21874999999999986, + "arcLengthMeters": 0.52499999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5451985339385683, + "yMeters": 1.0627455083814181, + "headingRadians": 0.22916666666666652, + "unwrappedHeadingRadians": 0.22916666666666652, + "arcLengthMeters": 0.54999999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5695149159569968, + "yMeters": 1.0685513600976548, + "headingRadians": 0.23958333333333318, + "unwrappedHeadingRadians": 0.23958333333333318, + "arcLengthMeters": 0.57499999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5937695022108547, + "yMeters": 1.0746101878944523, + "headingRadians": 0.24999999999999983, + "unwrappedHeadingRadians": 0.24999999999999983, + "arcLengthMeters": 0.59999999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6179596609328961, + "yMeters": 1.0809213343528641, + "headingRadians": 0.26041666666666652, + "unwrappedHeadingRadians": 0.26041666666666652, + "arcLengthMeters": 0.62499999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6420827673466467, + "yMeters": 1.0874841146758643, + "headingRadians": 0.2708333333333332, + "unwrappedHeadingRadians": 0.2708333333333332, + "arcLengthMeters": 0.6499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6661362039512069, + "yMeters": 1.0942978167626514, + "headingRadians": 0.28124999999999989, + "unwrappedHeadingRadians": 0.28124999999999989, + "arcLengthMeters": 0.67499999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6901173608052673, + "yMeters": 1.1013617012859171, + "headingRadians": 0.29166666666666657, + "unwrappedHeadingRadians": 0.29166666666666657, + "arcLengthMeters": 0.69999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7140236358103025, + "yMeters": 1.108675001772067, + "headingRadians": 0.30208333333333326, + "unwrappedHeadingRadians": 0.30208333333333326, + "arcLengthMeters": 0.72499999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7378524349929139, + "yMeters": 1.1162369246843864, + "headingRadians": 0.31249999999999994, + "unwrappedHeadingRadians": 0.31249999999999994, + "arcLengthMeters": 0.74999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7616011727862919, + "yMeters": 1.1240466495091457, + "headingRadians": 0.32291666666666663, + "unwrappedHeadingRadians": 0.32291666666666663, + "arcLengthMeters": 0.77499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7852672723107652, + "yMeters": 1.1321033288446294, + "headingRadians": 0.33333333333333331, + "unwrappedHeadingRadians": 0.33333333333333331, + "arcLengthMeters": 0.79999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8088481656534072, + "yMeters": 1.1404060884930853, + "headingRadians": 0.34375, + "unwrappedHeadingRadians": 0.34375, + "arcLengthMeters": 0.825, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8323412941466704, + "yMeters": 1.1489540275555796, + "headingRadians": 0.35416666666666669, + "unwrappedHeadingRadians": 0.35416666666666669, + "arcLengthMeters": 0.85, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8557441086460165, + "yMeters": 1.15774621852975, + "headingRadians": 0.36458333333333337, + "unwrappedHeadingRadians": 0.36458333333333337, + "arcLengthMeters": 0.875, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8790540698065141, + "yMeters": 1.1667817074104456, + "headingRadians": 0.37500000000000006, + "unwrappedHeadingRadians": 0.37500000000000006, + "arcLengthMeters": 0.9, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9022686483583733, + "yMeters": 1.1760595137932413, + "headingRadians": 0.38541666666666674, + "unwrappedHeadingRadians": 0.38541666666666674, + "arcLengthMeters": 0.925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9253853253813855, + "yMeters": 1.1855786309808183, + "headingRadians": 0.39583333333333343, + "unwrappedHeadingRadians": 0.39583333333333343, + "arcLengthMeters": 0.95000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9484015925782421, + "yMeters": 1.1953380260921962, + "headingRadians": 0.40625000000000011, + "unwrappedHeadingRadians": 0.40625000000000011, + "arcLengthMeters": 0.97500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9713149525466993, + "yMeters": 1.2053366401748073, + "headingRadians": 0.41666666666666674, + "unwrappedHeadingRadians": 0.41666666666666674, + "arcLengthMeters": 1.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9941229190505612, + "yMeters": 1.2155733883193989, + "headingRadians": 0.42708333333333343, + "unwrappedHeadingRadians": 0.42708333333333343, + "arcLengthMeters": 1.0250000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0168230172894512, + "yMeters": 1.2260471597777527, + "headingRadians": 0.43750000000000011, + "unwrappedHeadingRadians": 0.43750000000000011, + "arcLengthMeters": 1.0500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0394127841673431, + "yMeters": 1.2367568180832067, + "headingRadians": 0.4479166666666668, + "unwrappedHeadingRadians": 0.4479166666666668, + "arcLengthMeters": 1.0750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.061889768559821, + "yMeters": 1.2477012011739703, + "headingRadians": 0.45833333333333348, + "unwrappedHeadingRadians": 0.45833333333333348, + "arcLengthMeters": 1.1000000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0842515315800414, + "yMeters": 1.2588791215192134, + "headingRadians": 0.46875000000000017, + "unwrappedHeadingRadians": 0.46875000000000017, + "arcLengthMeters": 1.1250000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1064956468433662, + "yMeters": 1.2702893662479213, + "headingRadians": 0.47916666666666685, + "unwrappedHeadingRadians": 0.47916666666666685, + "arcLengthMeters": 1.1500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1286197007306416, + "yMeters": 1.281930697280498, + "headingRadians": 0.48958333333333354, + "unwrappedHeadingRadians": 0.48958333333333354, + "arcLengthMeters": 1.1750000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1506212926500878, + "yMeters": 1.2938018514631056, + "headingRadians": 0.50000000000000022, + "unwrappedHeadingRadians": 0.50000000000000022, + "arcLengthMeters": 1.2000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.17249803529778, + "yMeters": 1.3059015407047243, + "headingRadians": 0.51041666666666685, + "unwrappedHeadingRadians": 0.51041666666666685, + "arcLengthMeters": 1.225000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1942475549166827, + "yMeters": 1.3182284521169172, + "headingRadians": 0.52083333333333348, + "unwrappedHeadingRadians": 0.52083333333333348, + "arcLengthMeters": 1.2500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2158674915542185, + "yMeters": 1.3307812481562866, + "headingRadians": 0.53125000000000011, + "unwrappedHeadingRadians": 0.53125000000000011, + "arcLengthMeters": 1.2750000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.237355499318336, + "yMeters": 1.3435585667696071, + "headingRadians": 0.54166666666666674, + "unwrappedHeadingRadians": 0.54166666666666674, + "arcLengthMeters": 1.3000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2587092466320535, + "yMeters": 1.3565590215416132, + "headingRadians": 0.55208333333333337, + "unwrappedHeadingRadians": 0.55208333333333337, + "arcLengthMeters": 1.3250000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2799264164864486, + "yMeters": 1.3697812018454369, + "headingRadians": 0.5625, + "unwrappedHeadingRadians": 0.5625, + "arcLengthMeters": 1.3500000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3010047066920678, + "yMeters": 1.3832236729956673, + "headingRadians": 0.57291666666666663, + "unwrappedHeadingRadians": 0.57291666666666663, + "arcLengthMeters": 1.3750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3219418301287273, + "yMeters": 1.3968849764040232, + "headingRadians": 0.58333333333333326, + "unwrappedHeadingRadians": 0.58333333333333326, + "arcLengthMeters": 1.4000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3427355149936804, + "yMeters": 1.410763629737618, + "headingRadians": 0.59374999999999989, + "unwrappedHeadingRadians": 0.59374999999999989, + "arcLengthMeters": 1.4250000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3633835050481196, + "yMeters": 1.4248581270798029, + "headingRadians": 0.60416666666666652, + "unwrappedHeadingRadians": 0.60416666666666652, + "arcLengthMeters": 1.4500000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3838835598619945, + "yMeters": 1.4391669390935662, + "headingRadians": 0.61458333333333315, + "unwrappedHeadingRadians": 0.61458333333333315, + "arcLengthMeters": 1.475, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4042334550571089, + "yMeters": 1.4536885131874766, + "headingRadians": 0.62499999999999978, + "unwrappedHeadingRadians": 0.62499999999999978, + "arcLengthMeters": 1.5, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4244309825484822, + "yMeters": 1.4684212736841487, + "headingRadians": 0.63541666666666641, + "unwrappedHeadingRadians": 0.63541666666666641, + "arcLengthMeters": 1.525, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4444739507839373, + "yMeters": 1.4833636219912125, + "headingRadians": 0.645833333333333, + "unwrappedHeadingRadians": 0.645833333333333, + "arcLengthMeters": 1.5499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4643601849818983, + "yMeters": 1.4985139367747708, + "headingRadians": 0.65624999999999967, + "unwrappedHeadingRadians": 0.65624999999999967, + "arcLengthMeters": 1.5749999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4840875273673682, + "yMeters": 1.5138705741353242, + "headingRadians": 0.6666666666666663, + "unwrappedHeadingRadians": 0.6666666666666663, + "arcLengthMeters": 1.5999999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5036538374060568, + "yMeters": 1.5294318677861423, + "headingRadians": 0.67708333333333293, + "unwrappedHeadingRadians": 0.67708333333333293, + "arcLengthMeters": 1.6249999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5230569920366457, + "yMeters": 1.5451961292340677, + "headingRadians": 0.68749999999999956, + "unwrappedHeadingRadians": 0.68749999999999956, + "arcLengthMeters": 1.6499999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5422948859011503, + "yMeters": 1.5611616479627259, + "headingRadians": 0.69791666666666619, + "unwrappedHeadingRadians": 0.69791666666666619, + "arcLengthMeters": 1.6749999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5613654315733649, + "yMeters": 1.5773266916181279, + "headingRadians": 0.70833333333333282, + "unwrappedHeadingRadians": 0.70833333333333282, + "arcLengthMeters": 1.6999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5802665597853611, + "yMeters": 1.5936895061966398, + "headingRadians": 0.71874999999999944, + "unwrappedHeadingRadians": 0.71874999999999944, + "arcLengthMeters": 1.7249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5989962196520144, + "yMeters": 1.6102483162353034, + "headingRadians": 0.72916666666666607, + "unwrappedHeadingRadians": 0.72916666666666607, + "arcLengthMeters": 1.7499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6175523788935391, + "yMeters": 1.6270013250044848, + "headingRadians": 0.7395833333333327, + "unwrappedHeadingRadians": 0.7395833333333327, + "arcLengthMeters": 1.774999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6359330240560008, + "yMeters": 1.6439467147028288, + "headingRadians": 0.74999999999999933, + "unwrappedHeadingRadians": 0.74999999999999933, + "arcLengthMeters": 1.7999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6541361607297906, + "yMeters": 1.661082646654503, + "headingRadians": 0.760416666666666, + "unwrappedHeadingRadians": 0.760416666666666, + "arcLengthMeters": 1.8249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6721598137660285, + "yMeters": 1.6784072615087038, + "headingRadians": 0.77083333333333259, + "unwrappedHeadingRadians": 0.77083333333333259, + "arcLengthMeters": 1.8499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6900020274908796, + "yMeters": 1.6959186794414074, + "headingRadians": 0.78124999999999922, + "unwrappedHeadingRadians": 0.78124999999999922, + "arcLengthMeters": 1.8749999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7076608659177559, + "yMeters": 1.7136150003593427, + "headingRadians": 0.79166666666666585, + "unwrappedHeadingRadians": 0.79166666666666585, + "arcLengthMeters": 1.8999999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7251344129573836, + "yMeters": 1.731494304106161, + "headingRadians": 0.80208333333333248, + "unwrappedHeadingRadians": 0.80208333333333248, + "arcLengthMeters": 1.9249999999999985, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7424207726257084, + "yMeters": 1.7495546506707869, + "headingRadians": 0.81249999999999911, + "unwrappedHeadingRadians": 0.81249999999999911, + "arcLengthMeters": 1.9499999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7595180692496233, + "yMeters": 1.767794080397918, + "headingRadians": 0.82291666666666574, + "unwrappedHeadingRadians": 0.82291666666666574, + "arcLengthMeters": 1.9749999999999983, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7764244476704873, + "yMeters": 1.786210614200662, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 1.9999999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7932347537725639, + "yMeters": 1.8047150355305628, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.0249999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8100450598746405, + "yMeters": 1.8232194568604636, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.049999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.826855365976717, + "yMeters": 1.8417238781903644, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.074999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8436656720787936, + "yMeters": 1.8602282995202652, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.0999999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.86047597818087, + "yMeters": 1.8787327208501661, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.1249999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8772862842829467, + "yMeters": 1.8972371421800669, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.1499999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8940965903850233, + "yMeters": 1.9157415635099677, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 2.1749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 24.758836806279895, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 87, + "startsAtGearSwitch": false, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "s-bend", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:810b601e9090644292e4eb5652379f5f0db6fa7158aaf6e4faeae4b337ad5ec5", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [ + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 2400.0, + "centerYMm": 1300.0, + "radiusMm": 220.0 + }, + { + "kind": "axis-aligned-rectangle", + "xMinMm": 3000.0, + "xMaxMm": 3600.0, + "yMinMm": 2000.0, + "yMaxMm": 2600.0, + "centerXMm": 0.0, + "centerYMm": 0.0, + "radiusMm": 0.0 + }, + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 3720.0, + "centerYMm": 2320.0, + "radiusMm": 140.0 + }, + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 3720.0, + "centerYMm": 2680.0, + "radiusMm": 140.0 + } + ] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 1.0, + "headingRadians": 0.78539816339744828, + "unwrappedHeadingRadians": 0.78539816339744828, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.0175852794768074, + "yMeters": 1.0177694202026868, + "headingRadians": 0.79581483006411491, + "unwrappedHeadingRadians": 0.79581483006411491, + "arcLengthMeters": 0.024999999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0349845101200461, + "yMeters": 1.0357210530431966, + "headingRadians": 0.80623149673078154, + "unwrappedHeadingRadians": 0.80623149673078154, + "arcLengthMeters": 0.049999999999999829, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0521958040094341, + "yMeters": 1.0538529506623149, + "headingRadians": 0.81664816339744817, + "unwrappedHeadingRadians": 0.81664816339744817, + "arcLengthMeters": 0.074999999999999747, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.46938177489050215, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0692172936169484, + "yMeters": 1.0721631456410243, + "headingRadians": 0.8270648300641148, + "unwrappedHeadingRadians": 0.8270648300641148, + "arcLengthMeters": 0.099999999999999659, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.46938177489050215, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0860471320094625, + "yMeters": 1.0906496512139807, + "headingRadians": 0.83748149673078143, + "unwrappedHeadingRadians": 0.83748149673078143, + "arcLengthMeters": 0.12499999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.46938177489050215, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1026834930491489, + "yMeters": 1.1093104614850882, + "headingRadians": 0.84789816339744806, + "unwrappedHeadingRadians": 0.84789816339744806, + "arcLengthMeters": 0.14999999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.41395276481822574, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1191245715916278, + "yMeters": 1.1281435516451528, + "headingRadians": 0.85831483006411469, + "unwrappedHeadingRadians": 0.85831483006411469, + "arcLengthMeters": 0.17499999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.41395276481822574, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1353685836818339, + "yMeters": 1.1471468781915848, + "headingRadians": 0.86873149673078132, + "unwrappedHeadingRadians": 0.86873149673078132, + "arcLengthMeters": 0.19999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.41395276481822574, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1514137667475874, + "yMeters": 1.1663183791501308, + "headingRadians": 0.879148163397448, + "unwrappedHeadingRadians": 0.879148163397448, + "arcLengthMeters": 0.22499999999999923, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.36045082905681558, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1672583797908438, + "yMeters": 1.1856559742986119, + "headingRadians": 0.88956483006411458, + "unwrappedHeadingRadians": 0.88956483006411458, + "arcLengthMeters": 0.24999999999999914, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.36045082905681558, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.182900703576601, + "yMeters": 1.2051575653926381, + "headingRadians": 0.89998149673078121, + "unwrappedHeadingRadians": 0.89998149673078121, + "arcLengthMeters": 0.27499999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.198339040819449, + "yMeters": 1.2248210363932823, + "headingRadians": 0.91039816339744783, + "unwrappedHeadingRadians": 0.91039816339744783, + "arcLengthMeters": 0.299999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2135717163677331, + "yMeters": 1.2446442536966815, + "headingRadians": 0.92081483006411446, + "unwrappedHeadingRadians": 0.92081483006411446, + "arcLengthMeters": 0.3249999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2285970773853192, + "yMeters": 1.2646250663655463, + "headingRadians": 0.93123149673078109, + "unwrappedHeadingRadians": 0.93123149673078109, + "arcLengthMeters": 0.34999999999999881, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2434134935309367, + "yMeters": 1.2847613063625509, + "headingRadians": 0.94164816339744772, + "unwrappedHeadingRadians": 0.94164816339744772, + "arcLengthMeters": 0.37499999999999872, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2580193571350784, + "yMeters": 1.3050507887855771, + "headingRadians": 0.95206483006411435, + "unwrappedHeadingRadians": 0.95206483006411435, + "arcLengthMeters": 0.39999999999999863, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2724130833744447, + "yMeters": 1.3254913121047895, + "headingRadians": 0.962481496730781, + "unwrappedHeadingRadians": 0.962481496730781, + "arcLengthMeters": 0.42499999999999855, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2865931104439039, + "yMeters": 1.3460806584015148, + "headingRadians": 0.97289816339744761, + "unwrappedHeadingRadians": 0.97289816339744761, + "arcLengthMeters": 0.44999999999999846, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3005578997259597, + "yMeters": 1.3668165936088998, + "headingRadians": 0.98331483006411424, + "unwrappedHeadingRadians": 0.98331483006411424, + "arcLengthMeters": 0.47499999999999837, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3143059359576983, + "yMeters": 1.3876968677543193, + "headingRadians": 0.99373149673078087, + "unwrappedHeadingRadians": 0.99373149673078087, + "arcLengthMeters": 0.49999999999999828, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3278357273952051, + "yMeters": 1.4087192152035137, + "headingRadians": 1.0041481633974476, + "unwrappedHeadingRadians": 1.0041481633974476, + "arcLengthMeters": 0.52499999999999847, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3411458059754271, + "yMeters": 1.4298813549064222, + "headingRadians": 1.0145648300641144, + "unwrappedHeadingRadians": 1.0145648300641144, + "arcLengthMeters": 0.5499999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.21067092762809647, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.354234727475466, + "yMeters": 1.4511809906446904, + "headingRadians": 1.0249814967307811, + "unwrappedHeadingRadians": 1.0249814967307811, + "arcLengthMeters": 0.57499999999999873, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.367101071669286, + "yMeters": 1.4726158112808247, + "headingRadians": 1.0353981633974478, + "unwrappedHeadingRadians": 1.0353981633974478, + "arcLengthMeters": 0.59999999999999887, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3797434424818162, + "yMeters": 1.4941834910089637, + "headingRadians": 1.0458148300641146, + "unwrappedHeadingRadians": 1.0458148300641146, + "arcLengthMeters": 0.624999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3921604681404329, + "yMeters": 1.5158816896072425, + "headingRadians": 1.0562314967307813, + "unwrappedHeadingRadians": 1.0562314967307813, + "arcLengthMeters": 0.64999999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.404350801323806, + "yMeters": 1.53770805269172, + "headingRadians": 1.0666481633974481, + "unwrappedHeadingRadians": 1.0666481633974481, + "arcLengthMeters": 0.67499999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.16076258642584618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4163131193080905, + "yMeters": 1.5596602119718443, + "headingRadians": 1.0770648300641148, + "unwrappedHeadingRadians": 1.0770648300641148, + "arcLengthMeters": 0.6999999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.16076258642584618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4280461241104498, + "yMeters": 1.5817357855074268, + "headingRadians": 1.0874814967307815, + "unwrappedHeadingRadians": 1.0874814967307815, + "arcLengthMeters": 0.72499999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.16542738416163116, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4395485426298957, + "yMeters": 1.6039323779670949, + "headingRadians": 1.0978981633974483, + "unwrappedHeadingRadians": 1.0978981633974483, + "arcLengthMeters": 0.74999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.16542738416163116, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4508191267854267, + "yMeters": 1.626247580888202, + "headingRadians": 1.108314830064115, + "unwrappedHeadingRadians": 1.108314830064115, + "arcLengthMeters": 0.7749999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.12405453637069408, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4618566536514517, + "yMeters": 1.648678972938159, + "headingRadians": 1.1187314967307818, + "unwrappedHeadingRadians": 1.1187314967307818, + "arcLengthMeters": 0.79999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.12405453637069408, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4726599255904853, + "yMeters": 1.6712241201771632, + "headingRadians": 1.1291481633974485, + "unwrappedHeadingRadians": 1.1291481633974485, + "arcLengthMeters": 0.82500000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.12405453637069408, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4832277703830994, + "yMeters": 1.6938805763222957, + "headingRadians": 1.1395648300641152, + "unwrappedHeadingRadians": 1.1395648300641152, + "arcLengthMeters": 0.8500000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4935590413551147, + "yMeters": 1.7166458830129574, + "headingRadians": 1.149981496730782, + "unwrappedHeadingRadians": 1.149981496730782, + "arcLengthMeters": 0.87500000000000033, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5036526175020224, + "yMeters": 1.7395175700776162, + "headingRadians": 1.1603981633974487, + "unwrappedHeadingRadians": 1.1603981633974487, + "arcLengthMeters": 0.90000000000000047, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5135074036106191, + "yMeters": 1.7624931558018344, + "headingRadians": 1.1708148300641155, + "unwrappedHeadingRadians": 1.1708148300641155, + "arcLengthMeters": 0.9250000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5231223303778454, + "yMeters": 1.7855701471975509, + "headingRadians": 1.1812314967307822, + "unwrappedHeadingRadians": 1.1812314967307822, + "arcLengthMeters": 0.95000000000000073, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.12077891991816714, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5324963545268102, + "yMeters": 1.808746040273584, + "headingRadians": 1.1916481633974489, + "unwrappedHeadingRadians": 1.1916481633974489, + "arcLengthMeters": 0.97500000000000087, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.12077891991816714, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5416284589199927, + "yMeters": 1.83201832030733, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.0000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.14182540145670275, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5506392709464549, + "yMeters": 1.8553379529507434, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.0250000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.12241848545546363, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5596500829729172, + "yMeters": 1.878657585594157, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.0500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.12241848545546363, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5686608949993794, + "yMeters": 1.9019772182375705, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.0750000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.12241848545546363, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5776717070258417, + "yMeters": 1.925296850880984, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.1, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14977102437387113, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.586682519052304, + "yMeters": 1.9486164835243975, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.125, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14977102437387113, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5956933310787662, + "yMeters": 1.9719361161678111, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.15, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14977102437387113, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6047041431052285, + "yMeters": 1.9952557488112246, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.1749999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14022657692244167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6137149551316907, + "yMeters": 2.0185753814546383, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.2, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14022657692244167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.622725767158153, + "yMeters": 2.0418950140980519, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.2249999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.17314263913676164, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6317365791846152, + "yMeters": 2.0652146467414654, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.2499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.17314263913676164, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6407473912110775, + "yMeters": 2.0885342793848789, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.2749999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20772974675563327, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6497582032375397, + "yMeters": 2.1118539120282924, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.2999999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20772974675563327, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.658769015264002, + "yMeters": 2.1351735446717059, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.3249999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.21067092762809647, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6677798272904643, + "yMeters": 2.1584931773151195, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.3499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.21067092762809647, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6767906393169265, + "yMeters": 2.181812809958533, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.3749999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.2494263000700192, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6858014513433888, + "yMeters": 2.2051324426019465, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.3999999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.2494263000700192, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.694812263369851, + "yMeters": 2.22845207524536, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.4249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.28925537121869005, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7038230753963133, + "yMeters": 2.2517717078887736, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.4499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.28925537121869005, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7128338874227755, + "yMeters": 2.2750913405321871, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.474999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.30259972253743661, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7218446994492378, + "yMeters": 2.2984109731756006, + "headingRadians": 1.2020648300641155, + "unwrappedHeadingRadians": 1.2020648300641155, + "arcLengthMeters": 1.4999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.30259972253743661, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7309768038424205, + "yMeters": 2.321683253209347, + "headingRadians": 1.1916481633974487, + "unwrappedHeadingRadians": 1.1916481633974487, + "arcLengthMeters": 1.524999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30259972253743661, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7403508279913851, + "yMeters": 2.3448591462853803, + "headingRadians": 1.181231496730782, + "unwrappedHeadingRadians": 1.181231496730782, + "arcLengthMeters": 1.5499999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.34535575549757735, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7499657547586114, + "yMeters": 2.3679361376810966, + "headingRadians": 1.1708148300641152, + "unwrappedHeadingRadians": 1.1708148300641152, + "arcLengthMeters": 1.5749999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.34535575549757735, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7598205408672083, + "yMeters": 2.3909117234053148, + "headingRadians": 1.1603981633974485, + "unwrappedHeadingRadians": 1.1603981633974485, + "arcLengthMeters": 1.5999999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.3654321992066818, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7699141170141159, + "yMeters": 2.4137834104699731, + "headingRadians": 1.1499814967307818, + "unwrappedHeadingRadians": 1.1499814967307818, + "arcLengthMeters": 1.6249999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.3654321992066818, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7802453879861313, + "yMeters": 2.436548717160635, + "headingRadians": 1.139564830064115, + "unwrappedHeadingRadians": 1.139564830064115, + "arcLengthMeters": 1.6499999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7908132327787454, + "yMeters": 2.4592051733057674, + "headingRadians": 1.1291481633974483, + "unwrappedHeadingRadians": 1.1291481633974483, + "arcLengthMeters": 1.6749999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.801616504717779, + "yMeters": 2.4817503205447715, + "headingRadians": 1.1187314967307815, + "unwrappedHeadingRadians": 1.1187314967307815, + "arcLengthMeters": 1.7, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45578721930484167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.812654031583804, + "yMeters": 2.5041817125947285, + "headingRadians": 1.1083148300641148, + "unwrappedHeadingRadians": 1.1083148300641148, + "arcLengthMeters": 1.725, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.823924615739335, + "yMeters": 2.5264969155158359, + "headingRadians": 1.0978981633974481, + "unwrappedHeadingRadians": 1.0978981633974481, + "arcLengthMeters": 1.7500000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8354270342587808, + "yMeters": 2.548693507975504, + "headingRadians": 1.0874814967307813, + "unwrappedHeadingRadians": 1.0874814967307813, + "arcLengthMeters": 1.7750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8471600390611402, + "yMeters": 2.5707690815110862, + "headingRadians": 1.0770648300641146, + "unwrappedHeadingRadians": 1.0770648300641146, + "arcLengthMeters": 1.8000000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8591223570454247, + "yMeters": 2.592721240791211, + "headingRadians": 1.0666481633974478, + "unwrappedHeadingRadians": 1.0666481633974478, + "arcLengthMeters": 1.8250000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8713126902287978, + "yMeters": 2.6145476038756885, + "headingRadians": 1.0562314967307811, + "unwrappedHeadingRadians": 1.0562314967307811, + "arcLengthMeters": 1.8500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8837297158874144, + "yMeters": 2.6362458024739674, + "headingRadians": 1.0458148300641144, + "unwrappedHeadingRadians": 1.0458148300641144, + "arcLengthMeters": 1.8750000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8963720866999447, + "yMeters": 2.6578134822021067, + "headingRadians": 1.0353981633974476, + "unwrappedHeadingRadians": 1.0353981633974476, + "arcLengthMeters": 1.900000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9092384308937647, + "yMeters": 2.679248302838241, + "headingRadians": 1.0249814967307809, + "unwrappedHeadingRadians": 1.0249814967307809, + "arcLengthMeters": 1.9250000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9223273523938036, + "yMeters": 2.7005479385765092, + "headingRadians": 1.0145648300641141, + "unwrappedHeadingRadians": 1.0145648300641141, + "arcLengthMeters": 1.9500000000000013, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9356374309740256, + "yMeters": 2.7217100782794175, + "headingRadians": 1.0041481633974474, + "unwrappedHeadingRadians": 1.0041481633974474, + "arcLengthMeters": 1.9750000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9491672224115324, + "yMeters": 2.7427324257286121, + "headingRadians": 0.99373149673078076, + "unwrappedHeadingRadians": 0.99373149673078076, + "arcLengthMeters": 2.0000000000000013, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9630236351684296, + "yMeters": 2.7635405304050145, + "headingRadians": 0.97289816339744739, + "unwrappedHeadingRadians": 0.97289816339744739, + "arcLengthMeters": 2.0250000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9773105118228422, + "yMeters": 2.7840554652129836, + "headingRadians": 0.952064830064114, + "unwrappedHeadingRadians": 0.952064830064114, + "arcLengthMeters": 2.0500000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9920216516977221, + "yMeters": 2.8042683264229993, + "headingRadians": 0.93123149673078065, + "unwrappedHeadingRadians": 0.93123149673078065, + "arcLengthMeters": 2.0750000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0071506699806574, + "yMeters": 2.8241703414091313, + "headingRadians": 0.91039816339744728, + "unwrappedHeadingRadians": 0.91039816339744728, + "arcLengthMeters": 2.100000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.91000785125885864, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.02269100049496, + "yMeters": 2.8437528724564665, + "headingRadians": 0.88956483006411391, + "unwrappedHeadingRadians": 0.88956483006411391, + "arcLengthMeters": 2.1250000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.91000785125885864, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0386358985494648, + "yMeters": 2.8630074205099802, + "headingRadians": 0.86873149673078054, + "unwrappedHeadingRadians": 0.86873149673078054, + "arcLengthMeters": 2.1500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.91000785125885864, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0549784438658074, + "yMeters": 2.8819256288632285, + "headingRadians": 0.84789816339744717, + "unwrappedHeadingRadians": 0.84789816339744717, + "arcLengthMeters": 2.1750000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.862531247169067, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.071711543581908, + "yMeters": 2.9004992867852604, + "headingRadians": 0.8270648300641138, + "unwrappedHeadingRadians": 0.8270648300641138, + "arcLengthMeters": 2.2000000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.862531247169067, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0888279353303592, + "yMeters": 2.9187203330841744, + "headingRadians": 0.80623149673078043, + "unwrappedHeadingRadians": 0.80623149673078043, + "arcLengthMeters": 2.2250000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.86668295249990968, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1063201903903823, + "yMeters": 2.936580859605773, + "headingRadians": 0.78539816339744706, + "unwrappedHeadingRadians": 0.78539816339744706, + "arcLengthMeters": 2.2500000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.81693958304310133, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1241807169119808, + "yMeters": 2.9540731146657961, + "headingRadians": 0.76456483006411369, + "unwrappedHeadingRadians": 0.76456483006411369, + "arcLengthMeters": 2.2750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.81693958304310133, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1424017632108949, + "yMeters": 2.9711895064142473, + "headingRadians": 0.74373149673078032, + "unwrappedHeadingRadians": 0.74373149673078032, + "arcLengthMeters": 2.3000000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.82292972616222093, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1609754211329268, + "yMeters": 2.9879226061303479, + "headingRadians": 0.722898163397447, + "unwrappedHeadingRadians": 0.722898163397447, + "arcLengthMeters": 2.325, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.77341517170487128, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.179893629486175, + "yMeters": 3.0042651514466905, + "headingRadians": 0.70206483006411358, + "unwrappedHeadingRadians": 0.70206483006411358, + "arcLengthMeters": 2.35, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.77341517170487128, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1991481775396888, + "yMeters": 3.0202100495011952, + "headingRadians": 0.68123149673078021, + "unwrappedHeadingRadians": 0.68123149673078021, + "arcLengthMeters": 2.375, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.78134787466456534, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2187307085870245, + "yMeters": 3.0357503800154979, + "headingRadians": 0.66039816339744684, + "unwrappedHeadingRadians": 0.66039816339744684, + "arcLengthMeters": 2.4, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.73215463110637435, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2386327235731565, + "yMeters": 3.050879398298433, + "headingRadians": 0.63956483006411347, + "unwrappedHeadingRadians": 0.63956483006411347, + "arcLengthMeters": 2.425, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.73215463110637435, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2588455847831721, + "yMeters": 3.0655905381733128, + "headingRadians": 0.6187314967307801, + "unwrappedHeadingRadians": 0.6187314967307801, + "arcLengthMeters": 2.4499999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.69336801574440965, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2793605195911413, + "yMeters": 3.0798774148277257, + "headingRadians": 0.59789816339744672, + "unwrappedHeadingRadians": 0.59789816339744672, + "arcLengthMeters": 2.4749999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.69336801574440965, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3001686242675436, + "yMeters": 3.0937338275846229, + "headingRadians": 0.57706483006411347, + "unwrappedHeadingRadians": 0.57706483006411347, + "arcLengthMeters": 2.4999999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.64469762255002039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3212608678435949, + "yMeters": 3.1071537625934873, + "headingRadians": 0.5562314967307801, + "unwrappedHeadingRadians": 0.5562314967307801, + "arcLengthMeters": 2.5249999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.64469762255002039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3426280960307961, + "yMeters": 3.1201313954404166, + "headingRadians": 0.53539816339744672, + "unwrappedHeadingRadians": 0.53539816339744672, + "arcLengthMeters": 2.5499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.6572771079064933, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.364261035194005, + "yMeters": 3.13266109367599, + "headingRadians": 0.51456483006411335, + "unwrappedHeadingRadians": 0.51456483006411335, + "arcLengthMeters": 2.5749999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3861502963763059, + "yMeters": 3.144737419259819, + "headingRadians": 0.49373149673078004, + "unwrappedHeadingRadians": 0.49373149673078004, + "arcLengthMeters": 2.5999999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4082863793739309, + "yMeters": 3.1563551309207218, + "headingRadians": 0.47289816339744672, + "unwrappedHeadingRadians": 0.47289816339744672, + "arcLengthMeters": 2.6249999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.57678117552036834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4306596768594626, + "yMeters": 3.1675091864314995, + "headingRadians": 0.45206483006411341, + "unwrappedHeadingRadians": 0.45206483006411341, + "arcLengthMeters": 2.649999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.57678117552036834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4532604785515306, + "yMeters": 3.1781947447973233, + "headingRadians": 0.4312314967307801, + "unwrappedHeadingRadians": 0.4312314967307801, + "arcLengthMeters": 2.6749999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.52967160040373884, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4760789754291905, + "yMeters": 3.1884071683567847, + "headingRadians": 0.41039816339744678, + "unwrappedHeadingRadians": 0.41039816339744678, + "arcLengthMeters": 2.6999999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.52967160040373884, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4991052639891578, + "yMeters": 3.1981420247946963, + "headingRadians": 0.38956483006411347, + "unwrappedHeadingRadians": 0.38956483006411347, + "arcLengthMeters": 2.7249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.52967160040373884, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5223293505440472, + "yMeters": 3.20739508906577, + "headingRadians": 0.36873149673078015, + "unwrappedHeadingRadians": 0.36873149673078015, + "arcLengthMeters": 2.7499999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.50156757521136253, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5457411555597549, + "yMeters": 3.2161623452283381, + "headingRadians": 0.34789816339744684, + "unwrappedHeadingRadians": 0.34789816339744684, + "arcLengthMeters": 2.7749999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.50156757521136253, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5693305180300978, + "yMeters": 3.2244399881873211, + "headingRadians": 0.32706483006411352, + "unwrappedHeadingRadians": 0.32706483006411352, + "arcLengthMeters": 2.7999999999999985, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.45578721930484167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5930871998868117, + "yMeters": 3.2322244253456835, + "headingRadians": 0.30623149673078021, + "unwrappedHeadingRadians": 0.30623149673078021, + "arcLengthMeters": 2.8249999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.45578721930484167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6170008904429984, + "yMeters": 3.2395122781636649, + "headingRadians": 0.28539816339744689, + "unwrappedHeadingRadians": 0.28539816339744689, + "arcLengthMeters": 2.8499999999999983, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6410612108680871, + "yMeters": 3.2463003836251079, + "headingRadians": 0.26456483006411358, + "unwrappedHeadingRadians": 0.26456483006411358, + "arcLengthMeters": 2.8749999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.41039141141320834, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6652577186923732, + "yMeters": 3.2525857956102437, + "headingRadians": 0.24373149673078023, + "unwrappedHeadingRadians": 0.24373149673078023, + "arcLengthMeters": 2.8999999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.3654321992066818, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6895799123391746, + "yMeters": 3.2583657861743456, + "headingRadians": 0.22289816339744689, + "unwrappedHeadingRadians": 0.22289816339744689, + "arcLengthMeters": 2.924999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.38876462343047646, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7140172356826451, + "yMeters": 3.2636378467316884, + "headingRadians": 0.20206483006411355, + "unwrappedHeadingRadians": 0.20206483006411355, + "arcLengthMeters": 2.949999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.34535575549757735, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.738559082629259, + "yMeters": 3.2683996891443052, + "headingRadians": 0.18123149673078021, + "unwrappedHeadingRadians": 0.18123149673078021, + "arcLengthMeters": 2.9749999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.34535575549757735, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7631948017209842, + "yMeters": 3.2726492467150679, + "headingRadians": 0.160398163397447, + "unwrappedHeadingRadians": 0.160398163397447, + "arcLengthMeters": 2.9999999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.30259972253743661, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7879137007581436, + "yMeters": 3.2763846750846572, + "headingRadians": 0.13956483006411366, + "unwrappedHeadingRadians": 0.13956483006411366, + "arcLengthMeters": 3.0249999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.30259972253743661, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8127050514399596, + "yMeters": 3.2796043530320382, + "headingRadians": 0.11873149673078032, + "unwrappedHeadingRadians": 0.11873149673078032, + "arcLengthMeters": 3.0499999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.26058942819777364, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8375580940207645, + "yMeters": 3.2823068831780891, + "headingRadians": 0.097898163397446974, + "unwrappedHeadingRadians": 0.097898163397446974, + "arcLengthMeters": 3.0749999999999975, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.26058942819777364, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8624620419798625, + "yMeters": 3.2844910925920803, + "headingRadians": 0.077064830064113632, + "unwrappedHeadingRadians": 0.077064830064113632, + "arcLengthMeters": 3.0999999999999974, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.219434136036039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8874060867030078, + "yMeters": 3.2861560333007422, + "headingRadians": 0.056231496730780296, + "unwrappedHeadingRadians": 0.056231496730780296, + "arcLengthMeters": 3.1249999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.219434136036039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9123794021734746, + "yMeters": 3.2873009826996955, + "headingRadians": 0.035398163397446961, + "unwrappedHeadingRadians": 0.035398163397446961, + "arcLengthMeters": 3.1499999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.17926258267461259, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9373711496706796, + "yMeters": 3.28792544386707, + "headingRadians": 0.014564830064113625, + "unwrappedHeadingRadians": 0.014564830064113625, + "arcLengthMeters": 3.1749999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.17926258267461259, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9623704824743173, + "yMeters": 3.2880291457791762, + "headingRadians": -0.0062685032692197107, + "unwrappedHeadingRadians": -0.0062685032692197107, + "arcLengthMeters": 3.1999999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.14022657692244167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9873665505719678, + "yMeters": 3.2876120434281311, + "headingRadians": -0.027101836602553046, + "unwrappedHeadingRadians": -0.027101836602553046, + "arcLengthMeters": 3.224999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.14022657692244167, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.012348505368132, + "yMeters": 3.2866743178413937, + "headingRadians": -0.047935169935886382, + "unwrappedHeadingRadians": -0.047935169935886382, + "arcLengthMeters": 3.2499999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0373055043926551, + "yMeters": 3.2852163760031958, + "headingRadians": -0.068768503269219711, + "unwrappedHeadingRadians": -0.068768503269219711, + "arcLengthMeters": 3.2749999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0622267160064887, + "yMeters": 3.2832388506779071, + "headingRadians": -0.089601836602553053, + "unwrappedHeadingRadians": -0.089601836602553053, + "arcLengthMeters": 3.2999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.066308390518323823, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0871013241027518, + "yMeters": 3.2807426001354063, + "headingRadians": -0.1104351699358864, + "unwrappedHeadingRadians": -0.1104351699358864, + "arcLengthMeters": 3.3249999999999966, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.066308390518323823, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1119185328010546, + "yMeters": 3.2777287077785831, + "headingRadians": -0.13126850326921974, + "unwrappedHeadingRadians": -0.13126850326921974, + "arcLengthMeters": 3.3499999999999965, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.031882811685461809, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1366675711330378, + "yMeters": 3.2741984816731264, + "headingRadians": -0.15210183660255308, + "unwrappedHeadingRadians": -0.15210183660255308, + "arcLengthMeters": 3.3749999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.031882811685461809, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1613376977171055, + "yMeters": 3.2701534539798107, + "headingRadians": -0.17293516993588642, + "unwrappedHeadingRadians": -0.17293516993588642, + "arcLengthMeters": 3.3999999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.031882811685461809, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1859182054203137, + "yMeters": 3.265595380289517, + "headingRadians": -0.19376850326921977, + "unwrappedHeadingRadians": -0.19376850326921977, + "arcLengthMeters": 3.4249999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2103984260053946, + "yMeters": 3.2605262388612872, + "headingRadians": -0.21460183660255311, + "unwrappedHeadingRadians": -0.21460183660255311, + "arcLengthMeters": 3.4499999999999962, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2347677347609016, + "yMeters": 3.2549482297637344, + "headingRadians": -0.23543516993588645, + "unwrappedHeadingRadians": -0.23543516993588645, + "arcLengthMeters": 3.4749999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.259015555112462, + "yMeters": 3.2488637739201875, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 3.499999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2831656773872813, + "yMeters": 3.2424011163609188, + "headingRadians": -0.26668516993588637, + "unwrappedHeadingRadians": -0.26668516993588637, + "arcLengthMeters": 3.5249999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3072471713138136, + "yMeters": 3.2356872501957739, + "headingRadians": -0.27710183660255305, + "unwrappedHeadingRadians": -0.27710183660255305, + "arcLengthMeters": 3.5499999999999958, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3312574239063628, + "yMeters": 3.2287229039192691, + "headingRadians": -0.28751850326921974, + "unwrappedHeadingRadians": -0.28751850326921974, + "arcLengthMeters": 3.5749999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3551938299093425, + "yMeters": 3.221508833204505, + "headingRadians": -0.29793516993588642, + "unwrappedHeadingRadians": -0.29793516993588642, + "arcLengthMeters": 3.5999999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.379053792079961, + "yMeters": 3.2140458208211746, + "headingRadians": -0.30835183660255311, + "unwrappedHeadingRadians": -0.30835183660255311, + "arcLengthMeters": 3.6249999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4028347214700387, + "yMeters": 3.2063346765506253, + "headingRadians": -0.31876850326921979, + "unwrappedHeadingRadians": -0.31876850326921979, + "arcLengthMeters": 3.6499999999999955, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.426534037706924, + "yMeters": 3.1983762370979947, + "headingRadians": -0.32918516993588648, + "unwrappedHeadingRadians": -0.32918516993588648, + "arcLengthMeters": 3.6749999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4501491692734794, + "yMeters": 3.1901713660014219, + "headingRadians": -0.33960183660255316, + "unwrappedHeadingRadians": -0.33960183660255316, + "arcLengthMeters": 3.6999999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.473677553787105, + "yMeters": 3.1817209535383486, + "headingRadians": -0.35001850326921985, + "unwrappedHeadingRadians": -0.35001850326921985, + "arcLengthMeters": 3.7249999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4971166382777747, + "yMeters": 3.17302591662892, + "headingRadians": -0.36043516993588653, + "unwrappedHeadingRadians": -0.36043516993588653, + "arcLengthMeters": 3.7499999999999951, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5204638794650469, + "yMeters": 3.1640871987364911, + "headingRadians": -0.37085183660255322, + "unwrappedHeadingRadians": -0.37085183660255322, + "arcLengthMeters": 3.774999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5437167440340263, + "yMeters": 3.1549057697652581, + "headingRadians": -0.3812685032692199, + "unwrappedHeadingRadians": -0.3812685032692199, + "arcLengthMeters": 3.7999999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.566872708910243, + "yMeters": 3.1454826259550157, + "headingRadians": -0.39168516993588659, + "unwrappedHeadingRadians": -0.39168516993588659, + "arcLengthMeters": 3.8249999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5899292615334217, + "yMeters": 3.1358187897730607, + "headingRadians": -0.40210183660255328, + "unwrappedHeadingRadians": -0.40210183660255328, + "arcLengthMeters": 3.8499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6128839001301105, + "yMeters": 3.1259153098032471, + "headingRadians": -0.41251850326921996, + "unwrappedHeadingRadians": -0.41251850326921996, + "arcLengthMeters": 3.8749999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6357341339851357, + "yMeters": 3.1157732606322091, + "headingRadians": -0.42293516993588665, + "unwrappedHeadingRadians": -0.42293516993588665, + "arcLengthMeters": 3.8999999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6584774837118608, + "yMeters": 3.1053937427327614, + "headingRadians": -0.43335183660255333, + "unwrappedHeadingRadians": -0.43335183660255333, + "arcLengthMeters": 3.9249999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6811114815212158, + "yMeters": 3.0947778823444927, + "headingRadians": -0.44376850326922, + "unwrappedHeadingRadians": -0.44376850326922, + "arcLengthMeters": 3.9499999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7036336714894644, + "yMeters": 3.0839268313515604, + "headingRadians": -0.4541851699358867, + "unwrappedHeadingRadians": -0.4541851699358867, + "arcLengthMeters": 3.9749999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7260416098246885, + "yMeters": 3.0728417671577062, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 3.9999999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7483916108321265, + "yMeters": 3.0616400951857408, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.0249999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7707416118395645, + "yMeters": 3.0504384232137753, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.0499999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7930916128470025, + "yMeters": 3.03923675124181, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.0749999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8154416138544405, + "yMeters": 3.0280350792698445, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.0999999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8377916148618785, + "yMeters": 3.0168334072978791, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.124999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8601416158693165, + "yMeters": 3.0056317353259137, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.1499999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8824916168767545, + "yMeters": 2.9944300633539482, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.1749999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9048416178841925, + "yMeters": 2.9832283913819828, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.19999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9271916188916305, + "yMeters": 2.9720267194100174, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.22499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9495416198990685, + "yMeters": 2.960825047438052, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.2499999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9718916209065065, + "yMeters": 2.9496233754660866, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.2749999999999888, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9942416219139445, + "yMeters": 2.9384217034941211, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.2999999999999883, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0165916229213829, + "yMeters": 2.9272200315221557, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.3249999999999886, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0389416239288209, + "yMeters": 2.9160183595501903, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.3499999999999881, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0612916249362589, + "yMeters": 2.9048166875782249, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.3749999999999876, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.083641625943697, + "yMeters": 2.8936150156062594, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.399999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.105991626951135, + "yMeters": 2.882413343634294, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.4249999999999865, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.128341627958573, + "yMeters": 2.8712116716623286, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.449999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.150691628966011, + "yMeters": 2.8600099996903632, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.4749999999999854, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.173041629973449, + "yMeters": 2.8488083277183978, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 4.4999999999999849, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.195449568308673, + "yMeters": 2.8377232635245431, + "headingRadians": -0.45418516993588665, + "unwrappedHeadingRadians": -0.45418516993588665, + "arcLengthMeters": 4.5249999999999853, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2179717582769216, + "yMeters": 2.8268722125316113, + "headingRadians": -0.44376850326921996, + "unwrappedHeadingRadians": -0.44376850326921996, + "arcLengthMeters": 4.5499999999999856, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2406057560862767, + "yMeters": 2.8162563521433426, + "headingRadians": -0.43335183660255328, + "unwrappedHeadingRadians": -0.43335183660255328, + "arcLengthMeters": 4.574999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2633491058130017, + "yMeters": 2.8058768342438949, + "headingRadians": -0.42293516993588659, + "unwrappedHeadingRadians": -0.42293516993588659, + "arcLengthMeters": 4.5999999999999863, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2861993396680269, + "yMeters": 2.7957347850728569, + "headingRadians": -0.4125185032692199, + "unwrappedHeadingRadians": -0.4125185032692199, + "arcLengthMeters": 4.6249999999999867, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3091539782647157, + "yMeters": 2.7858313051030432, + "headingRadians": -0.40210183660255322, + "unwrappedHeadingRadians": -0.40210183660255322, + "arcLengthMeters": 4.649999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3322105308878944, + "yMeters": 2.7761674689210882, + "headingRadians": -0.39168516993588653, + "unwrappedHeadingRadians": -0.39168516993588653, + "arcLengthMeters": 4.6749999999999874, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3553664957641116, + "yMeters": 2.7667443251108459, + "headingRadians": -0.38126850326921985, + "unwrappedHeadingRadians": -0.38126850326921985, + "arcLengthMeters": 4.6999999999999877, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.378619360333091, + "yMeters": 2.7575628961396128, + "headingRadians": -0.37085183660255316, + "unwrappedHeadingRadians": -0.37085183660255316, + "arcLengthMeters": 4.7249999999999881, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4019666015203631, + "yMeters": 2.7486241782471841, + "headingRadians": -0.36043516993588648, + "unwrappedHeadingRadians": -0.36043516993588648, + "arcLengthMeters": 4.7499999999999885, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4254056860110333, + "yMeters": 2.7399291413377553, + "headingRadians": -0.35001850326921979, + "unwrappedHeadingRadians": -0.35001850326921979, + "arcLengthMeters": 4.7749999999999888, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4489340705246594, + "yMeters": 2.731478728874682, + "headingRadians": -0.33960183660255311, + "unwrappedHeadingRadians": -0.33960183660255311, + "arcLengthMeters": 4.7999999999999892, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4725492020912148, + "yMeters": 2.7232738577781088, + "headingRadians": -0.32918516993588642, + "unwrappedHeadingRadians": -0.32918516993588642, + "arcLengthMeters": 4.8249999999999895, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4962485183281, + "yMeters": 2.7153154183254782, + "headingRadians": -0.31876850326921974, + "unwrappedHeadingRadians": -0.31876850326921974, + "arcLengthMeters": 4.84999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5200294477181782, + "yMeters": 2.7076042740549289, + "headingRadians": -0.30835183660255305, + "unwrappedHeadingRadians": -0.30835183660255305, + "arcLengthMeters": 4.87499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5438894098887967, + "yMeters": 2.7001412616715985, + "headingRadians": -0.29793516993588637, + "unwrappedHeadingRadians": -0.29793516993588637, + "arcLengthMeters": 4.8999999999999906, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5678258158917764, + "yMeters": 2.6929271909568344, + "headingRadians": -0.28751850326921968, + "unwrappedHeadingRadians": -0.28751850326921968, + "arcLengthMeters": 4.9249999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5918360684843256, + "yMeters": 2.6859628446803296, + "headingRadians": -0.277101836602553, + "unwrappedHeadingRadians": -0.277101836602553, + "arcLengthMeters": 4.9499999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6159175624108579, + "yMeters": 2.6792489785151847, + "headingRadians": -0.26668516993588631, + "unwrappedHeadingRadians": -0.26668516993588631, + "arcLengthMeters": 4.9749999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6400676846856772, + "yMeters": 2.672786320955916, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 4.999999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6400676846856772, + "yMeters": 2.672786320955916, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 4.999999999999992, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 4.615884121107797, + "yMeters": 2.6791231381912954, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.0249999999999915, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5917005575299168, + "yMeters": 2.6854599554266749, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.0499999999999909, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5675169939520366, + "yMeters": 2.6917967726620544, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.07499999999999, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5433334303741564, + "yMeters": 2.6981335898974339, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.09999999999999, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5191498667962762, + "yMeters": 2.7044704071328134, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.1249999999999893, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.494966303218396, + "yMeters": 2.7108072243681929, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.1499999999999888, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4707827396405158, + "yMeters": 2.7171440416035724, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.1749999999999883, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4465991760626356, + "yMeters": 2.7234808588389519, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.1999999999999877, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4224156124847553, + "yMeters": 2.7298176760743313, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.2249999999999872, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3982320489068751, + "yMeters": 2.7361544933097108, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.2499999999999867, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3740484853289949, + "yMeters": 2.7424913105450903, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.2749999999999861, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3498649217511147, + "yMeters": 2.74882812778047, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.2999999999999856, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3256813581732345, + "yMeters": 2.7551649450158493, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.3249999999999851, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3014977945953543, + "yMeters": 2.7615017622512288, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.3499999999999845, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2773142310174741, + "yMeters": 2.7678385794866083, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.374999999999984, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2531306674395939, + "yMeters": 2.7741753967219878, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.3999999999999835, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2289471038617137, + "yMeters": 2.7805122139573673, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.4249999999999829, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2047635402838335, + "yMeters": 2.7868490311927467, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.4499999999999824, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1805799767059533, + "yMeters": 2.7931858484281262, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.4749999999999819, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1563964131280731, + "yMeters": 2.7995226656635057, + "headingRadians": -0.25626850326921968, + "unwrappedHeadingRadians": -0.25626850326921968, + "arcLengthMeters": 5.4999999999999813, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1322462908532538, + "yMeters": 2.8059853232227745, + "headingRadians": -0.26668516993588637, + "unwrappedHeadingRadians": -0.26668516993588637, + "arcLengthMeters": 5.5249999999999817, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1081647969267214, + "yMeters": 2.8126991893879194, + "headingRadians": -0.27710183660255305, + "unwrappedHeadingRadians": -0.27710183660255305, + "arcLengthMeters": 5.5499999999999821, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0841545443341722, + "yMeters": 2.8196635356644242, + "headingRadians": -0.28751850326921974, + "unwrappedHeadingRadians": -0.28751850326921974, + "arcLengthMeters": 5.5749999999999824, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0602181383311926, + "yMeters": 2.8268776063791883, + "headingRadians": -0.29793516993588642, + "unwrappedHeadingRadians": -0.29793516993588642, + "arcLengthMeters": 5.5999999999999828, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.036358176160574, + "yMeters": 2.8343406187625186, + "headingRadians": -0.30835183660255311, + "unwrappedHeadingRadians": -0.30835183660255311, + "arcLengthMeters": 5.6249999999999831, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0125772467704959, + "yMeters": 2.8420517630330679, + "headingRadians": -0.31876850326921979, + "unwrappedHeadingRadians": -0.31876850326921979, + "arcLengthMeters": 5.6499999999999835, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9888779305336106, + "yMeters": 2.8500102024856986, + "headingRadians": -0.32918516993588648, + "unwrappedHeadingRadians": -0.32918516993588648, + "arcLengthMeters": 5.6749999999999838, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9652627989670552, + "yMeters": 2.8582150735822713, + "headingRadians": -0.33960183660255316, + "unwrappedHeadingRadians": -0.33960183660255316, + "arcLengthMeters": 5.6999999999999842, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9417344144534296, + "yMeters": 2.8666654860453447, + "headingRadians": -0.35001850326921985, + "unwrappedHeadingRadians": -0.35001850326921985, + "arcLengthMeters": 5.7249999999999845, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.91829532996276, + "yMeters": 2.8753605229547734, + "headingRadians": -0.36043516993588653, + "unwrappedHeadingRadians": -0.36043516993588653, + "arcLengthMeters": 5.7499999999999849, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8949480887754877, + "yMeters": 2.8842992408472021, + "headingRadians": -0.37085183660255322, + "unwrappedHeadingRadians": -0.37085183660255322, + "arcLengthMeters": 5.7749999999999853, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8716952242065084, + "yMeters": 2.8934806698184352, + "headingRadians": -0.3812685032692199, + "unwrappedHeadingRadians": -0.3812685032692199, + "arcLengthMeters": 5.7999999999999856, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8485392593302916, + "yMeters": 2.9029038136286776, + "headingRadians": -0.39168516993588659, + "unwrappedHeadingRadians": -0.39168516993588659, + "arcLengthMeters": 5.824999999999986, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8254827067071129, + "yMeters": 2.9125676498106325, + "headingRadians": -0.40210183660255328, + "unwrappedHeadingRadians": -0.40210183660255328, + "arcLengthMeters": 5.8499999999999863, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8025280681104241, + "yMeters": 2.9224711297804462, + "headingRadians": -0.41251850326921996, + "unwrappedHeadingRadians": -0.41251850326921996, + "arcLengthMeters": 5.8749999999999867, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7796778342553989, + "yMeters": 2.9326131789514842, + "headingRadians": -0.42293516993588665, + "unwrappedHeadingRadians": -0.42293516993588665, + "arcLengthMeters": 5.899999999999987, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7569344845286738, + "yMeters": 2.9429926968509319, + "headingRadians": -0.43335183660255333, + "unwrappedHeadingRadians": -0.43335183660255333, + "arcLengthMeters": 5.9249999999999874, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7343004867193188, + "yMeters": 2.9536085572392006, + "headingRadians": -0.44376850326922, + "unwrappedHeadingRadians": -0.44376850326922, + "arcLengthMeters": 5.9499999999999877, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.71177829675107, + "yMeters": 2.9644596082321328, + "headingRadians": -0.4541851699358867, + "unwrappedHeadingRadians": -0.4541851699358867, + "arcLengthMeters": 5.9749999999999881, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6893703584158462, + "yMeters": 2.9755446724259871, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 5.9999999999999885, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6893703584158462, + "yMeters": 2.9755446724259871, + "headingRadians": -0.46460183660255333, + "unwrappedHeadingRadians": -0.46460183660255333, + "arcLengthMeters": 5.9999999999999885, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 3.7118354225675825, + "yMeters": 2.9645766148325938, + "headingRadians": -0.44376850326922, + "unwrappedHeadingRadians": -0.44376850326922, + "arcLengthMeters": 6.0249999999999888, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7345240963356225, + "yMeters": 2.9540789256887359, + "headingRadians": -0.4229351699358867, + "unwrappedHeadingRadians": -0.4229351699358867, + "arcLengthMeters": 6.0499999999999892, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7574265325614795, + "yMeters": 2.94405616111831, + "headingRadians": -0.40210183660255339, + "unwrappedHeadingRadians": -0.40210183660255339, + "arcLengthMeters": 6.0749999999999895, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7805327913111775, + "yMeters": 2.9345126711222114, + "headingRadians": -0.38126850326922007, + "unwrappedHeadingRadians": -0.38126850326922007, + "arcLengthMeters": 6.09999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8038328441893032, + "yMeters": 2.9254525976903807, + "headingRadians": -0.36043516993588676, + "unwrappedHeadingRadians": -0.36043516993588676, + "arcLengthMeters": 6.12499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8273165786914509, + "yMeters": 2.9168798730041297, + "headingRadians": -0.33960183660255344, + "unwrappedHeadingRadians": -0.33960183660255344, + "arcLengthMeters": 6.1499999999999906, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8509738025931712, + "yMeters": 2.9087982177295277, + "headingRadians": -0.31876850326922013, + "unwrappedHeadingRadians": -0.31876850326922013, + "arcLengthMeters": 6.1749999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8747942483735192, + "yMeters": 2.9012111394025881, + "headingRadians": -0.29793516993588681, + "unwrappedHeadingRadians": -0.29793516993588681, + "arcLengthMeters": 6.1999999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8987675776712836, + "yMeters": 2.8941219309069535, + "headingRadians": -0.2771018366025535, + "unwrappedHeadingRadians": -0.2771018366025535, + "arcLengthMeters": 6.2249999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9228833857719594, + "yMeters": 2.8875336690447466, + "headingRadians": -0.25626850326922018, + "unwrappedHeadingRadians": -0.25626850326922018, + "arcLengthMeters": 6.249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9471312061235198, + "yMeters": 2.8814492132011997, + "headingRadians": -0.23543516993588684, + "unwrappedHeadingRadians": -0.23543516993588684, + "arcLengthMeters": 6.2749999999999924, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9715005148790268, + "yMeters": 2.8758712041036469, + "headingRadians": -0.2146018366025535, + "unwrappedHeadingRadians": -0.2146018366025535, + "arcLengthMeters": 6.2999999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9959807354641077, + "yMeters": 2.8708020626754172, + "headingRadians": -0.19376850326922015, + "unwrappedHeadingRadians": -0.19376850326922015, + "arcLengthMeters": 6.3249999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0205612431673154, + "yMeters": 2.8662439889851234, + "headingRadians": -0.17293516993588681, + "unwrappedHeadingRadians": -0.17293516993588681, + "arcLengthMeters": 6.3499999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0452313697513835, + "yMeters": 2.8621989612918077, + "headingRadians": -0.15210183660255347, + "unwrappedHeadingRadians": -0.15210183660255347, + "arcLengthMeters": 6.3749999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0699804080833664, + "yMeters": 2.8586687351863511, + "headingRadians": -0.13126850326922013, + "unwrappedHeadingRadians": -0.13126850326922013, + "arcLengthMeters": 6.3999999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0947976167816691, + "yMeters": 2.8556548428295274, + "headingRadians": -0.11043516993588678, + "unwrappedHeadingRadians": -0.11043516993588678, + "arcLengthMeters": 6.4249999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1196722248779327, + "yMeters": 2.8531585922870266, + "headingRadians": -0.089601836602553442, + "unwrappedHeadingRadians": -0.089601836602553442, + "arcLengthMeters": 6.4499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1445934364917658, + "yMeters": 2.8511810669617379, + "headingRadians": -0.0687685032692201, + "unwrappedHeadingRadians": -0.0687685032692201, + "arcLengthMeters": 6.4749999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1695504355162889, + "yMeters": 2.8497231251235404, + "headingRadians": -0.047935169935886882, + "unwrappedHeadingRadians": -0.047935169935886882, + "arcLengthMeters": 6.4999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1945323903124532, + "yMeters": 2.8487853995368031, + "headingRadians": -0.027101836602553546, + "unwrappedHeadingRadians": -0.027101836602553546, + "arcLengthMeters": 6.524999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2195284584101032, + "yMeters": 2.8483682971857576, + "headingRadians": -0.00626850326922021, + "unwrappedHeadingRadians": -0.00626850326922021, + "arcLengthMeters": 6.5499999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2445277912137414, + "yMeters": 2.8484719990978635, + "headingRadians": 0.014564830064113125, + "unwrappedHeadingRadians": 0.014564830064113125, + "arcLengthMeters": 6.5749999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2695195387109468, + "yMeters": 2.8490964602652382, + "headingRadians": 0.035398163397446461, + "unwrappedHeadingRadians": 0.035398163397446461, + "arcLengthMeters": 6.5999999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2944928541814136, + "yMeters": 2.8502414096641915, + "headingRadians": 0.0562314967307798, + "unwrappedHeadingRadians": 0.0562314967307798, + "arcLengthMeters": 6.6249999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3194368989045584, + "yMeters": 2.8519063503728534, + "headingRadians": 0.077064830064113132, + "unwrappedHeadingRadians": 0.077064830064113132, + "arcLengthMeters": 6.6499999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3443408468636564, + "yMeters": 2.8540905597868447, + "headingRadians": 0.097898163397446475, + "unwrappedHeadingRadians": 0.097898163397446475, + "arcLengthMeters": 6.6749999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3691938894444613, + "yMeters": 2.8567930899328955, + "headingRadians": 0.11873149673077982, + "unwrappedHeadingRadians": 0.11873149673077982, + "arcLengthMeters": 6.6999999999999975, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3939852401262769, + "yMeters": 2.8600127678802765, + "headingRadians": 0.13956483006411316, + "unwrappedHeadingRadians": 0.13956483006411316, + "arcLengthMeters": 6.7249999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4187041391634363, + "yMeters": 2.8637481962498659, + "headingRadians": 0.1603981633974465, + "unwrappedHeadingRadians": 0.1603981633974465, + "arcLengthMeters": 6.7499999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.443339858255162, + "yMeters": 2.8679977538206285, + "headingRadians": 0.18123149673077985, + "unwrappedHeadingRadians": 0.18123149673077985, + "arcLengthMeters": 6.7749999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4678817052017763, + "yMeters": 2.8727595962332453, + "headingRadians": 0.20206483006411319, + "unwrappedHeadingRadians": 0.20206483006411319, + "arcLengthMeters": 6.7999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4923190285452472, + "yMeters": 2.8780316567905881, + "headingRadians": 0.22289816339744653, + "unwrappedHeadingRadians": 0.22289816339744653, + "arcLengthMeters": 6.8249999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5166412221920487, + "yMeters": 2.88381164735469, + "headingRadians": 0.24373149673077987, + "unwrappedHeadingRadians": 0.24373149673077987, + "arcLengthMeters": 6.85, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5408377300163343, + "yMeters": 2.8900970593398259, + "headingRadians": 0.26456483006411319, + "unwrappedHeadingRadians": 0.26456483006411319, + "arcLengthMeters": 6.875, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5648980504414229, + "yMeters": 2.8968851648012688, + "headingRadians": 0.2853981633974465, + "unwrappedHeadingRadians": 0.2853981633974465, + "arcLengthMeters": 6.9, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.58881174099761, + "yMeters": 2.90417301761925, + "headingRadians": 0.30623149673077982, + "unwrappedHeadingRadians": 0.30623149673077982, + "arcLengthMeters": 6.9250000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6125684228543236, + "yMeters": 2.9119574547776126, + "headingRadians": 0.32706483006411313, + "unwrappedHeadingRadians": 0.32706483006411313, + "arcLengthMeters": 6.9500000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6361577853246665, + "yMeters": 2.9202350977365956, + "headingRadians": 0.34789816339744645, + "unwrappedHeadingRadians": 0.34789816339744645, + "arcLengthMeters": 6.9750000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6595695903403742, + "yMeters": 2.9290023538991639, + "headingRadians": 0.36873149673077965, + "unwrappedHeadingRadians": 0.36873149673077965, + "arcLengthMeters": 7.0000000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6827936768952636, + "yMeters": 2.9382554181702374, + "headingRadians": 0.38956483006411297, + "unwrappedHeadingRadians": 0.38956483006411297, + "arcLengthMeters": 7.0250000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.70581996545523, + "yMeters": 2.9479902746081486, + "headingRadians": 0.41039816339744628, + "unwrappedHeadingRadians": 0.41039816339744628, + "arcLengthMeters": 7.0500000000000016, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.72863846233289, + "yMeters": 2.95820269816761, + "headingRadians": 0.4312314967307796, + "unwrappedHeadingRadians": 0.4312314967307796, + "arcLengthMeters": 7.075000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7512392640249583, + "yMeters": 2.9688882565334338, + "headingRadians": 0.45206483006411291, + "unwrappedHeadingRadians": 0.45206483006411291, + "arcLengthMeters": 7.1000000000000023, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7736125615104905, + "yMeters": 2.9800423120442114, + "headingRadians": 0.47289816339744623, + "unwrappedHeadingRadians": 0.47289816339744623, + "arcLengthMeters": 7.1250000000000027, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7957486445081159, + "yMeters": 2.9916600237051139, + "headingRadians": 0.49373149673077954, + "unwrappedHeadingRadians": 0.49373149673077954, + "arcLengthMeters": 7.150000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8176379056904173, + "yMeters": 3.0037363492889426, + "headingRadians": 0.51456483006411291, + "unwrappedHeadingRadians": 0.51456483006411291, + "arcLengthMeters": 7.1750000000000034, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8392708448536261, + "yMeters": 3.0162660475245158, + "headingRadians": 0.53539816339744628, + "unwrappedHeadingRadians": 0.53539816339744628, + "arcLengthMeters": 7.2000000000000037, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8606380730408274, + "yMeters": 3.0292436803714451, + "headingRadians": 0.55623149673077965, + "unwrappedHeadingRadians": 0.55623149673077965, + "arcLengthMeters": 7.2250000000000041, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8817303166168786, + "yMeters": 3.0426636153803095, + "headingRadians": 0.577064830064113, + "unwrappedHeadingRadians": 0.577064830064113, + "arcLengthMeters": 7.2500000000000044, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.902538421293281, + "yMeters": 3.0565200281372067, + "headingRadians": 0.59789816339744639, + "unwrappedHeadingRadians": 0.59789816339744639, + "arcLengthMeters": 7.2750000000000048, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.92305335610125, + "yMeters": 3.0708069047916196, + "headingRadians": 0.61873149673077976, + "unwrappedHeadingRadians": 0.61873149673077976, + "arcLengthMeters": 7.3000000000000052, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9432662173112654, + "yMeters": 3.0855180446664994, + "headingRadians": 0.63956483006411313, + "unwrappedHeadingRadians": 0.63956483006411313, + "arcLengthMeters": 7.3250000000000055, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9631682322973978, + "yMeters": 3.1006470629494345, + "headingRadians": 0.6603981633974465, + "unwrappedHeadingRadians": 0.6603981633974465, + "arcLengthMeters": 7.3500000000000059, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9827507633447334, + "yMeters": 3.1161873934637372, + "headingRadians": 0.68123149673077987, + "unwrappedHeadingRadians": 0.68123149673077987, + "arcLengthMeters": 7.3750000000000062, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 5.0020053113982472, + "yMeters": 3.1321322915182419, + "headingRadians": 0.70206483006411324, + "unwrappedHeadingRadians": 0.70206483006411324, + "arcLengthMeters": 7.4000000000000066, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 200, + "startsAtGearSwitch": false, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 1, + "direction": 1, + "startIndex": 201, + "endIndex": 241, + "startsAtGearSwitch": true, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 2, + "direction": 0, + "startIndex": 242, + "endIndex": 298, + "startsAtGearSwitch": true, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "large-heading-change", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:44b6c95a3b553bb04df3912d5cf7c9110f4d629e37551b2c130d8d7f22706fce", + "map": { + "xMinMm": -7000.0, + "xMaxMm": 11000.0, + "yMinMm": -7000.0, + "yMaxMm": 11000.0, + "resolutionMm": 50.0, + "obstacles": [] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 1.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.0249995478901843, + "yMeters": 1.0001302071559617, + "headingRadians": 0.010416666666666666, + "unwrappedHeadingRadians": 0.010416666666666666, + "arcLengthMeters": 0.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499963831803427, + "yMeters": 1.0005208144955946, + "headingRadians": 0.020833333333333332, + "unwrappedHeadingRadians": 0.020833333333333332, + "arcLengthMeters": 0.05, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749877935647825, + "yMeters": 1.0011717796356725, + "headingRadians": 0.03125, + "unwrappedHeadingRadians": 0.03125, + "arcLengthMeters": 0.075000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999710673264458, + "yMeters": 1.0020830319425964, + "headingRadians": 0.041666666666666664, + "unwrappedHeadingRadians": 0.041666666666666664, + "arcLengthMeters": 0.1, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249434936311464, + "yMeters": 1.0032544725400563, + "headingRadians": 0.052083333333333329, + "unwrappedHeadingRadians": 0.052083333333333329, + "arcLengthMeters": 0.125, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499023628217124, + "yMeters": 1.004685974319762, + "headingRadians": 0.062499999999999993, + "unwrappedHeadingRadians": 0.062499999999999993, + "arcLengthMeters": 0.15, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1748449667119993, + "yMeters": 1.0063773819552337, + "headingRadians": 0.072916666666666657, + "unwrappedHeadingRadians": 0.072916666666666657, + "arcLengthMeters": 0.175, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1997685988807447, + "yMeters": 1.0083285119186571, + "headingRadians": 0.083333333333333329, + "unwrappedHeadingRadians": 0.083333333333333329, + "arcLengthMeters": 0.2, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.224670554965231, + "yMeters": 1.0105391525007958, + "headingRadians": 0.09375, + "unwrappedHeadingRadians": 0.09375, + "arcLengthMeters": 0.22500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2495481329547253, + "yMeters": 1.0130090638339635, + "headingRadians": 0.10416666666666667, + "unwrappedHeadingRadians": 0.10416666666666667, + "arcLengthMeters": 0.25000000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2743986334836632, + "yMeters": 1.0157379779180522, + "headingRadians": 0.11458333333333334, + "unwrappedHeadingRadians": 0.11458333333333334, + "arcLengthMeters": 0.27500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2992193601245465, + "yMeters": 1.0187255986496104, + "headingRadians": 0.125, + "unwrappedHeadingRadians": 0.125, + "arcLengthMeters": 0.30000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3240076196805208, + "yMeters": 1.0219716018539722, + "headingRadians": 0.13541666666666666, + "unwrappedHeadingRadians": 0.13541666666666666, + "arcLengthMeters": 0.325, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3487607224776048, + "yMeters": 1.0254756353204335, + "headingRadians": 0.14583333333333331, + "unwrappedHeadingRadians": 0.14583333333333331, + "arcLengthMeters": 0.35, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3734759826565346, + "yMeters": 1.0292373188404673, + "headingRadians": 0.15624999999999997, + "unwrappedHeadingRadians": 0.15624999999999997, + "arcLengthMeters": 0.37499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3981507184641961, + "yMeters": 1.0332562442489797, + "headingRadians": 0.16666666666666663, + "unwrappedHeadingRadians": 0.16666666666666663, + "arcLengthMeters": 0.39999999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4227822525446114, + "yMeters": 1.0375319754685981, + "headingRadians": 0.17708333333333329, + "unwrappedHeadingRadians": 0.17708333333333329, + "arcLengthMeters": 0.42499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4473679122294476, + "yMeters": 1.0420640485569872, + "headingRadians": 0.18749999999999994, + "unwrappedHeadingRadians": 0.18749999999999994, + "arcLengthMeters": 0.44999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4719050298280172, + "yMeters": 1.0468519717571914, + "headingRadians": 0.1979166666666666, + "unwrappedHeadingRadians": 0.1979166666666666, + "arcLengthMeters": 0.47499999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963909429167384, + "yMeters": 1.051895225550991, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.49999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5207947384276534, + "yMeters": 1.0573203669662046, + "headingRadians": 0.22916666666666655, + "unwrappedHeadingRadians": 0.22916666666666655, + "arcLengthMeters": 0.52499999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5450802225637967, + "yMeters": 1.0632527067227218, + "headingRadians": 0.24999999999999989, + "unwrappedHeadingRadians": 0.24999999999999989, + "arcLengthMeters": 0.54999999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5692368551316926, + "yMeters": 1.0696896701134277, + "headingRadians": 0.2708333333333332, + "unwrappedHeadingRadians": 0.2708333333333332, + "arcLengthMeters": 0.57499999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5932541518610028, + "yMeters": 1.0766284634184542, + "headingRadians": 0.29166666666666652, + "unwrappedHeadingRadians": 0.29166666666666652, + "arcLengthMeters": 0.59999999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6171216889548261, + "yMeters": 1.0840660751176889, + "headingRadians": 0.31249999999999983, + "unwrappedHeadingRadians": 0.31249999999999983, + "arcLengthMeters": 0.62499999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6408291076137518, + "yMeters": 1.0919992771978104, + "headingRadians": 0.33333333333333315, + "unwrappedHeadingRadians": 0.33333333333333315, + "arcLengthMeters": 0.6499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6643661185317042, + "yMeters": 1.1004246265532853, + "headingRadians": 0.35416666666666646, + "unwrappedHeadingRadians": 0.35416666666666646, + "arcLengthMeters": 0.67499999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6877225063616261, + "yMeters": 1.1093384664807182, + "headingRadians": 0.37499999999999978, + "unwrappedHeadingRadians": 0.37499999999999978, + "arcLengthMeters": 0.69999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7108881341490618, + "yMeters": 1.1187369282659045, + "headingRadians": 0.39583333333333309, + "unwrappedHeadingRadians": 0.39583333333333309, + "arcLengthMeters": 0.72499999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7338529477317186, + "yMeters": 1.1286159328628991, + "headingRadians": 0.41666666666666641, + "unwrappedHeadingRadians": 0.41666666666666641, + "arcLengthMeters": 0.74999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7566069801030946, + "yMeters": 1.1389711926643717, + "headingRadians": 0.43749999999999972, + "unwrappedHeadingRadians": 0.43749999999999972, + "arcLengthMeters": 0.77499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7791403557382794, + "yMeters": 1.1497982133624807, + "headingRadians": 0.45833333333333304, + "unwrappedHeadingRadians": 0.45833333333333304, + "arcLengthMeters": 0.79999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8014432948800518, + "yMeters": 1.1610922958994561, + "headingRadians": 0.47916666666666635, + "unwrappedHeadingRadians": 0.47916666666666635, + "arcLengthMeters": 0.825, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8235061177834124, + "yMeters": 1.1728485385070482, + "headingRadians": 0.49999999999999967, + "unwrappedHeadingRadians": 0.49999999999999967, + "arcLengthMeters": 0.85, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8453192489167098, + "yMeters": 1.185061838833954, + "headingRadians": 0.520833333333333, + "unwrappedHeadingRadians": 0.520833333333333, + "arcLengthMeters": 0.875, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8668732211175367, + "yMeters": 1.1977268961602989, + "headingRadians": 0.54166666666666641, + "unwrappedHeadingRadians": 0.54166666666666641, + "arcLengthMeters": 0.9, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8881586797015932, + "yMeters": 1.2108382136982139, + "headingRadians": 0.56249999999999978, + "unwrappedHeadingRadians": 0.56249999999999978, + "arcLengthMeters": 0.925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9091663865227326, + "yMeters": 1.2243901009775071, + "headingRadians": 0.58333333333333315, + "unwrappedHeadingRadians": 0.58333333333333315, + "arcLengthMeters": 0.95000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.929887223982429, + "yMeters": 1.2383766763153972, + "headingRadians": 0.60416666666666652, + "unwrappedHeadingRadians": 0.60416666666666652, + "arcLengthMeters": 0.97500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9503121989869237, + "yMeters": 1.2527918693692341, + "headingRadians": 0.62499999999999978, + "unwrappedHeadingRadians": 0.62499999999999978, + "arcLengthMeters": 1.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9705097264782969, + "yMeters": 1.2675246298659062, + "headingRadians": 0.63541666666666641, + "unwrappedHeadingRadians": 0.63541666666666641, + "arcLengthMeters": 1.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9905526947137522, + "yMeters": 1.28246697817297, + "headingRadians": 0.645833333333333, + "unwrappedHeadingRadians": 0.645833333333333, + "arcLengthMeters": 1.0499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0104389289117135, + "yMeters": 1.2976172929565284, + "headingRadians": 0.65624999999999967, + "unwrappedHeadingRadians": 0.65624999999999967, + "arcLengthMeters": 1.0749999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0301662712971833, + "yMeters": 1.3129739303170818, + "headingRadians": 0.6666666666666663, + "unwrappedHeadingRadians": 0.6666666666666663, + "arcLengthMeters": 1.0999999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.049732581335872, + "yMeters": 1.3285352239678998, + "headingRadians": 0.67708333333333293, + "unwrappedHeadingRadians": 0.67708333333333293, + "arcLengthMeters": 1.1249999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0691357359664608, + "yMeters": 1.3442994854158252, + "headingRadians": 0.68749999999999956, + "unwrappedHeadingRadians": 0.68749999999999956, + "arcLengthMeters": 1.1499999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0883736298309654, + "yMeters": 1.3602650041444835, + "headingRadians": 0.69791666666666619, + "unwrappedHeadingRadians": 0.69791666666666619, + "arcLengthMeters": 1.1749999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.10744417550318, + "yMeters": 1.3764300477998854, + "headingRadians": 0.70833333333333282, + "unwrappedHeadingRadians": 0.70833333333333282, + "arcLengthMeters": 1.1999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1263453037151763, + "yMeters": 1.3927928623783974, + "headingRadians": 0.71874999999999944, + "unwrappedHeadingRadians": 0.71874999999999944, + "arcLengthMeters": 1.2249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1450749635818296, + "yMeters": 1.4093516724170609, + "headingRadians": 0.72916666666666607, + "unwrappedHeadingRadians": 0.72916666666666607, + "arcLengthMeters": 1.2499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1636311228233542, + "yMeters": 1.4261046811862423, + "headingRadians": 0.7395833333333327, + "unwrappedHeadingRadians": 0.7395833333333327, + "arcLengthMeters": 1.274999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.182011767985816, + "yMeters": 1.4430500708845864, + "headingRadians": 0.74999999999999933, + "unwrappedHeadingRadians": 0.74999999999999933, + "arcLengthMeters": 1.2999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2002149046596058, + "yMeters": 1.4601860028362605, + "headingRadians": 0.760416666666666, + "unwrappedHeadingRadians": 0.760416666666666, + "arcLengthMeters": 1.3249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2182385576958437, + "yMeters": 1.4775106176904613, + "headingRadians": 0.77083333333333259, + "unwrappedHeadingRadians": 0.77083333333333259, + "arcLengthMeters": 1.3499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2360807714206947, + "yMeters": 1.4950220356231649, + "headingRadians": 0.78124999999999922, + "unwrappedHeadingRadians": 0.78124999999999922, + "arcLengthMeters": 1.3749999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.253739609847571, + "yMeters": 1.5127183565411002, + "headingRadians": 0.79166666666666585, + "unwrappedHeadingRadians": 0.79166666666666585, + "arcLengthMeters": 1.3999999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2712131568871987, + "yMeters": 1.5305976602879185, + "headingRadians": 0.80208333333333248, + "unwrappedHeadingRadians": 0.80208333333333248, + "arcLengthMeters": 1.4249999999999985, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2884995165555235, + "yMeters": 1.5486580068525444, + "headingRadians": 0.81249999999999911, + "unwrappedHeadingRadians": 0.81249999999999911, + "arcLengthMeters": 1.4499999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3055968131794384, + "yMeters": 1.5668974365796755, + "headingRadians": 0.82291666666666574, + "unwrappedHeadingRadians": 0.82291666666666574, + "arcLengthMeters": 1.4749999999999983, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3225031916003025, + "yMeters": 1.5853139703824195, + "headingRadians": 0.83333333333333237, + "unwrappedHeadingRadians": 0.83333333333333237, + "arcLengthMeters": 1.4999999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3392168173752395, + "yMeters": 1.6039056099570359, + "headingRadians": 0.843749999999999, + "unwrappedHeadingRadians": 0.843749999999999, + "arcLengthMeters": 1.5249999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3557358769761847, + "yMeters": 1.6226703379997627, + "headingRadians": 0.85416666666666563, + "unwrappedHeadingRadians": 0.85416666666666563, + "arcLengthMeters": 1.549999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.372058577986663, + "yMeters": 1.6416061184257078, + "headingRadians": 0.86458333333333226, + "unwrappedHeadingRadians": 0.86458333333333226, + "arcLengthMeters": 1.574999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3881831492962777, + "yMeters": 1.660710896589775, + "headingRadians": 0.87499999999999889, + "unwrappedHeadingRadians": 0.87499999999999889, + "arcLengthMeters": 1.5999999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4041078412928862, + "yMeters": 1.6799825995096063, + "headingRadians": 0.88541666666666552, + "unwrappedHeadingRadians": 0.88541666666666552, + "arcLengthMeters": 1.6249999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4198309260524429, + "yMeters": 1.6994191360905115, + "headingRadians": 0.89583333333333215, + "unwrappedHeadingRadians": 0.89583333333333215, + "arcLengthMeters": 1.6499999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.43535069752649, + "yMeters": 1.7190183973523663, + "headingRadians": 0.90624999999999878, + "unwrappedHeadingRadians": 0.90624999999999878, + "arcLengthMeters": 1.6749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4506654717272736, + "yMeters": 1.7387782566584469, + "headingRadians": 0.91666666666666541, + "unwrappedHeadingRadians": 0.91666666666666541, + "arcLengthMeters": 1.6999999999999975, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.465773586910466, + "yMeters": 1.7586965699461847, + "headingRadians": 0.927083333333332, + "unwrappedHeadingRadians": 0.927083333333332, + "arcLengthMeters": 1.7249999999999974, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4806734037554756, + "yMeters": 1.778771175959809, + "headingRadians": 0.93749999999999867, + "unwrappedHeadingRadians": 0.93749999999999867, + "arcLengthMeters": 1.7499999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4953633055433229, + "yMeters": 1.7989998964848568, + "headingRadians": 0.9479166666666653, + "unwrappedHeadingRadians": 0.9479166666666653, + "arcLengthMeters": 1.7749999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5098416983320635, + "yMeters": 1.8193805365845206, + "headingRadians": 0.95833333333333193, + "unwrappedHeadingRadians": 0.95833333333333193, + "arcLengthMeters": 1.7999999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5241070111297406, + "yMeters": 1.8399108848378143, + "headingRadians": 0.96874999999999856, + "unwrappedHeadingRadians": 0.96874999999999856, + "arcLengthMeters": 1.8249999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5381576960648475, + "yMeters": 1.8605887135795232, + "headingRadians": 0.97916666666666519, + "unwrappedHeadingRadians": 0.97916666666666519, + "arcLengthMeters": 1.849999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.55199222855428, + "yMeters": 1.8814117791419207, + "headingRadians": 0.98958333333333182, + "unwrappedHeadingRadians": 0.98958333333333182, + "arcLengthMeters": 1.8749999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5656091074687639, + "yMeters": 1.9023778220982197, + "headingRadians": 0.99999999999999845, + "unwrappedHeadingRadians": 0.99999999999999845, + "arcLengthMeters": 1.8999999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5790068552957353, + "yMeters": 1.9234845675077321, + "headingRadians": 1.0104166666666652, + "unwrappedHeadingRadians": 1.0104166666666652, + "arcLengthMeters": 1.9249999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.59218401829966, + "yMeters": 1.9447297251627149, + "headingRadians": 1.0208333333333319, + "unwrappedHeadingRadians": 1.0208333333333319, + "arcLengthMeters": 1.9499999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.605139166679773, + "yMeters": 1.966110989836872, + "headingRadians": 1.0312499999999987, + "unwrappedHeadingRadians": 1.0312499999999987, + "arcLengthMeters": 1.9749999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6178708947252187, + "yMeters": 1.9876260415354823, + "headingRadians": 1.0416666666666652, + "unwrappedHeadingRadians": 1.0416666666666652, + "arcLengthMeters": 1.9999999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6303778209675825, + "yMeters": 2.0092725457471365, + "headingRadians": 1.0520833333333319, + "unwrappedHeadingRadians": 1.0520833333333319, + "arcLengthMeters": 2.0249999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6426585883307836, + "yMeters": 2.0310481536970433, + "headingRadians": 1.0624999999999987, + "unwrappedHeadingRadians": 1.0624999999999987, + "arcLengthMeters": 2.0499999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6547118642783287, + "yMeters": 2.0529505026018851, + "headingRadians": 1.0729166666666654, + "unwrappedHeadingRadians": 1.0729166666666654, + "arcLengthMeters": 2.074999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6665363409579013, + "yMeters": 2.0749772159261952, + "headingRadians": 1.0833333333333321, + "unwrappedHeadingRadians": 1.0833333333333321, + "arcLengthMeters": 2.0999999999999983, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6781307353432684, + "yMeters": 2.0971259036402263, + "headingRadians": 1.0937499999999989, + "unwrappedHeadingRadians": 1.0937499999999989, + "arcLengthMeters": 2.1249999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6894937893734978, + "yMeters": 2.1193941624792818, + "headingRadians": 1.1041666666666656, + "unwrappedHeadingRadians": 1.1041666666666656, + "arcLengthMeters": 2.149999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7006242700894663, + "yMeters": 2.1417795762044856, + "headingRadians": 1.1145833333333324, + "unwrappedHeadingRadians": 1.1145833333333324, + "arcLengthMeters": 2.1749999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7115209697676415, + "yMeters": 2.1642797158649576, + "headingRadians": 1.1249999999999991, + "unwrappedHeadingRadians": 1.1249999999999991, + "arcLengthMeters": 2.1999999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7221827060511288, + "yMeters": 2.1868921400613694, + "headingRadians": 1.1354166666666659, + "unwrappedHeadingRadians": 1.1354166666666659, + "arcLengthMeters": 2.225, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7326083220779616, + "yMeters": 2.2096143952108509, + "headingRadians": 1.1458333333333326, + "unwrappedHeadingRadians": 1.1458333333333326, + "arcLengthMeters": 2.2500000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7427966866066296, + "yMeters": 2.2324440158132188, + "headingRadians": 1.1562499999999993, + "unwrappedHeadingRadians": 1.1562499999999993, + "arcLengthMeters": 2.2750000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7527466941388261, + "yMeters": 2.2553785247184979, + "headingRadians": 1.1666666666666661, + "unwrappedHeadingRadians": 1.1666666666666661, + "arcLengthMeters": 2.3000000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7624572650393984, + "yMeters": 2.2784154333957067, + "headingRadians": 1.1770833333333328, + "unwrappedHeadingRadians": 1.1770833333333328, + "arcLengthMeters": 2.3250000000000015, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7719273456534967, + "yMeters": 2.3015522422028774, + "headingRadians": 1.1874999999999996, + "unwrappedHeadingRadians": 1.1874999999999996, + "arcLengthMeters": 2.3500000000000019, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7811559084209012, + "yMeters": 2.3247864406582828, + "headingRadians": 1.1979166666666663, + "unwrappedHeadingRadians": 1.1979166666666663, + "arcLengthMeters": 2.3750000000000022, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7901419519875197, + "yMeters": 2.3481155077128379, + "headingRadians": 1.208333333333333, + "unwrappedHeadingRadians": 1.208333333333333, + "arcLengthMeters": 2.4000000000000026, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.798884501314038, + "yMeters": 2.3715369120236494, + "headingRadians": 1.2187499999999998, + "unwrappedHeadingRadians": 1.2187499999999998, + "arcLengthMeters": 2.4250000000000029, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.80738260778172, + "yMeters": 2.39504811222868, + "headingRadians": 1.2291666666666665, + "unwrappedHeadingRadians": 1.2291666666666665, + "arcLengthMeters": 2.4500000000000033, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8156353492953374, + "yMeters": 2.418646557222504, + "headingRadians": 1.2395833333333333, + "unwrappedHeadingRadians": 1.2395833333333333, + "arcLengthMeters": 2.4750000000000036, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8236418303832216, + "yMeters": 2.4423296864331134, + "headingRadians": 1.2499999999999998, + "unwrappedHeadingRadians": 1.2499999999999998, + "arcLengthMeters": 2.5000000000000031, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8314011822944303, + "yMeters": 2.4660949300997608, + "headingRadians": 1.2604166666666665, + "unwrappedHeadingRadians": 1.2604166666666665, + "arcLengthMeters": 2.5250000000000035, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8389125630930097, + "yMeters": 2.4899397095517881, + "headingRadians": 1.2708333333333333, + "unwrappedHeadingRadians": 1.2708333333333333, + "arcLengthMeters": 2.5500000000000038, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.84617515774935, + "yMeters": 2.5138614374884316, + "headingRadians": 1.28125, + "unwrappedHeadingRadians": 1.28125, + "arcLengthMeters": 2.5750000000000042, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8531881782286228, + "yMeters": 2.5378575182595577, + "headingRadians": 1.2916666666666667, + "unwrappedHeadingRadians": 1.2916666666666667, + "arcLengthMeters": 2.6000000000000045, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8599508635762847, + "yMeters": 2.5619253481473065, + "headingRadians": 1.3020833333333335, + "unwrappedHeadingRadians": 1.3020833333333335, + "arcLengthMeters": 2.6250000000000049, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8664624800006475, + "yMeters": 2.5860623156486118, + "headingRadians": 1.3125000000000002, + "unwrappedHeadingRadians": 1.3125000000000002, + "arcLengthMeters": 2.6500000000000052, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8727223209524988, + "yMeters": 2.6102658017585632, + "headingRadians": 1.322916666666667, + "unwrappedHeadingRadians": 1.322916666666667, + "arcLengthMeters": 2.6750000000000056, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8787297072017655, + "yMeters": 2.6345331802545857, + "headingRadians": 1.3333333333333337, + "unwrappedHeadingRadians": 1.3333333333333337, + "arcLengthMeters": 2.700000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8844839869112162, + "yMeters": 2.6588618179813985, + "headingRadians": 1.3437500000000004, + "unwrappedHeadingRadians": 1.3437500000000004, + "arcLengthMeters": 2.7250000000000063, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8899845357071881, + "yMeters": 2.6832490751367293, + "headingRadians": 1.3541666666666672, + "unwrappedHeadingRadians": 1.3541666666666672, + "arcLengthMeters": 2.7500000000000067, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8952307567473343, + "yMeters": 2.7076923055577482, + "headingRadians": 1.3645833333333339, + "unwrappedHeadingRadians": 1.3645833333333339, + "arcLengthMeters": 2.775000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9002220807853885, + "yMeters": 2.7321888570081918, + "headingRadians": 1.3750000000000007, + "unwrappedHeadingRadians": 1.3750000000000007, + "arcLengthMeters": 2.8000000000000074, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.904957966232927, + "yMeters": 2.7567360714661477, + "headingRadians": 1.3854166666666674, + "unwrappedHeadingRadians": 1.3854166666666674, + "arcLengthMeters": 2.8250000000000077, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9094378992181373, + "yMeters": 2.781331285412465, + "headingRadians": 1.3958333333333341, + "unwrappedHeadingRadians": 1.3958333333333341, + "arcLengthMeters": 2.8500000000000081, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9136613936415756, + "yMeters": 2.8059718301197614, + "headingRadians": 1.4062500000000009, + "unwrappedHeadingRadians": 1.4062500000000009, + "arcLengthMeters": 2.8750000000000084, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9176279912289109, + "yMeters": 2.8306550319419976, + "headingRadians": 1.4166666666666676, + "unwrappedHeadingRadians": 1.4166666666666676, + "arcLengthMeters": 2.9000000000000088, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9213372615806508, + "yMeters": 2.8553782126045824, + "headingRadians": 1.4270833333333344, + "unwrappedHeadingRadians": 1.4270833333333344, + "arcLengthMeters": 2.9250000000000091, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9247888022188429, + "yMeters": 2.8801386894949816, + "headingRadians": 1.4375000000000011, + "unwrappedHeadingRadians": 1.4375000000000011, + "arcLengthMeters": 2.9500000000000095, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.927982238630745, + "yMeters": 2.9049337759537992, + "headingRadians": 1.4479166666666679, + "unwrappedHeadingRadians": 1.4479166666666679, + "arcLengthMeters": 2.97500000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.930917224309463, + "yMeters": 2.9297607815662929, + "headingRadians": 1.4583333333333344, + "unwrappedHeadingRadians": 1.4583333333333344, + "arcLengthMeters": 3.0000000000000093, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9335934407915487, + "yMeters": 2.9546170124543036, + "headingRadians": 1.4687500000000011, + "unwrappedHeadingRadians": 1.4687500000000011, + "arcLengthMeters": 3.0250000000000097, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9360105976915549, + "yMeters": 2.9794997715685545, + "headingRadians": 1.4791666666666679, + "unwrappedHeadingRadians": 1.4791666666666679, + "arcLengthMeters": 3.05000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9381684327335438, + "yMeters": 3.0044063589812984, + "headingRadians": 1.4895833333333346, + "unwrappedHeadingRadians": 1.4895833333333346, + "arcLengthMeters": 3.0750000000000104, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 25.45584412271571, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 123, + "startsAtGearSwitch": false, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "rectangle-detour", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:6ad46b2782bc519b17d62dac9e238542a88f4265fb7dd3a1ebe84c04313cf6ce", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [ + { + "kind": "axis-aligned-rectangle", + "xMinMm": 2700.0, + "xMaxMm": 3300.0, + "yMinMm": 1200.0, + "yMaxMm": 2800.0, + "centerXMm": 0.0, + "centerYMm": 0.0, + "radiusMm": 0.0 + } + ] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.0249995478901843, + "yMeters": 2.0001302071559617, + "headingRadians": 0.010416666666666666, + "unwrappedHeadingRadians": 0.010416666666666666, + "arcLengthMeters": 0.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499963831803427, + "yMeters": 2.0005208144955944, + "headingRadians": 0.020833333333333332, + "unwrappedHeadingRadians": 0.020833333333333332, + "arcLengthMeters": 0.05, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749877935647825, + "yMeters": 2.0011717796356723, + "headingRadians": 0.03125, + "unwrappedHeadingRadians": 0.03125, + "arcLengthMeters": 0.075000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999710673264458, + "yMeters": 2.002083031942596, + "headingRadians": 0.041666666666666664, + "unwrappedHeadingRadians": 0.041666666666666664, + "arcLengthMeters": 0.1, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249434936311464, + "yMeters": 2.0032544725400561, + "headingRadians": 0.052083333333333329, + "unwrappedHeadingRadians": 0.052083333333333329, + "arcLengthMeters": 0.125, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499023628217124, + "yMeters": 2.0046859743197616, + "headingRadians": 0.062499999999999993, + "unwrappedHeadingRadians": 0.062499999999999993, + "arcLengthMeters": 0.15, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1748449667119993, + "yMeters": 2.0063773819552333, + "headingRadians": 0.072916666666666657, + "unwrappedHeadingRadians": 0.072916666666666657, + "arcLengthMeters": 0.175, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1997685988807447, + "yMeters": 2.0083285119186565, + "headingRadians": 0.083333333333333329, + "unwrappedHeadingRadians": 0.083333333333333329, + "arcLengthMeters": 0.2, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.224670554965231, + "yMeters": 2.0105391525007952, + "headingRadians": 0.09375, + "unwrappedHeadingRadians": 0.09375, + "arcLengthMeters": 0.22500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2495481329547253, + "yMeters": 2.0130090638339628, + "headingRadians": 0.10416666666666667, + "unwrappedHeadingRadians": 0.10416666666666667, + "arcLengthMeters": 0.25000000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2743986334836632, + "yMeters": 2.0157379779180515, + "headingRadians": 0.11458333333333334, + "unwrappedHeadingRadians": 0.11458333333333334, + "arcLengthMeters": 0.27500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2992193601245465, + "yMeters": 2.0187255986496098, + "headingRadians": 0.125, + "unwrappedHeadingRadians": 0.125, + "arcLengthMeters": 0.30000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3240076196805208, + "yMeters": 2.0219716018539717, + "headingRadians": 0.13541666666666666, + "unwrappedHeadingRadians": 0.13541666666666666, + "arcLengthMeters": 0.325, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3487607224776048, + "yMeters": 2.0254756353204333, + "headingRadians": 0.14583333333333331, + "unwrappedHeadingRadians": 0.14583333333333331, + "arcLengthMeters": 0.35, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3734759826565346, + "yMeters": 2.0292373188404671, + "headingRadians": 0.15624999999999997, + "unwrappedHeadingRadians": 0.15624999999999997, + "arcLengthMeters": 0.37499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3981507184641961, + "yMeters": 2.0332562442489794, + "headingRadians": 0.16666666666666663, + "unwrappedHeadingRadians": 0.16666666666666663, + "arcLengthMeters": 0.39999999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4227822525446114, + "yMeters": 2.0375319754685979, + "headingRadians": 0.17708333333333329, + "unwrappedHeadingRadians": 0.17708333333333329, + "arcLengthMeters": 0.42499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4473679122294476, + "yMeters": 2.0420640485569868, + "headingRadians": 0.18749999999999994, + "unwrappedHeadingRadians": 0.18749999999999994, + "arcLengthMeters": 0.44999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4719050298280172, + "yMeters": 2.0468519717571909, + "headingRadians": 0.1979166666666666, + "unwrappedHeadingRadians": 0.1979166666666666, + "arcLengthMeters": 0.47499999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963909429167384, + "yMeters": 2.0518952255509908, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.49999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5208503676505822, + "yMeters": 2.0570659645397069, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.52499999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5453097923844261, + "yMeters": 2.062236703528423, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.54999999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.56976921711827, + "yMeters": 2.0674074425171391, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.57499999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5942286418521139, + "yMeters": 2.0725781815058553, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.59999999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6186880665859578, + "yMeters": 2.0777489204945714, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.62499999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6431474913198016, + "yMeters": 2.0829196594832875, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.6499999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6676069160536455, + "yMeters": 2.0880903984720036, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.67499999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6920663407874894, + "yMeters": 2.0932611374607197, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.69999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7165257655213333, + "yMeters": 2.0984318764494359, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.72499999999999987, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7409851902551772, + "yMeters": 2.103602615438152, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.74999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7654446149890211, + "yMeters": 2.1087733544268681, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.77499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7899040397228649, + "yMeters": 2.1139440934155842, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.79999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8143634644567088, + "yMeters": 2.1191148324043003, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.825, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8388228891905527, + "yMeters": 2.1242855713930164, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.85, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8632823139243966, + "yMeters": 2.1294563103817326, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.875, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8877417386582405, + "yMeters": 2.1346270493704487, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.9, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9122011633920843, + "yMeters": 2.1397977883591648, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9366605881259282, + "yMeters": 2.1449685273478809, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.95000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9611200128597721, + "yMeters": 2.150139266336597, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.97500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9855794375936158, + "yMeters": 2.1553100053253131, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.99999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9855794375936158, + "yMeters": 2.1553100053253131, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.99999999999999989, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 1.9611473858823298, + "yMeters": 2.1500119681610288, + "headingRadians": 0.21874999999999986, + "unwrappedHeadingRadians": 0.21874999999999986, + "arcLengthMeters": 1.025, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9367718465717858, + "yMeters": 2.144459722494886, + "headingRadians": 0.22916666666666652, + "unwrappedHeadingRadians": 0.22916666666666652, + "arcLengthMeters": 1.0499999999999998, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9124554645533574, + "yMeters": 2.1386538707786493, + "headingRadians": 0.23958333333333318, + "unwrappedHeadingRadians": 0.23958333333333318, + "arcLengthMeters": 1.0749999999999997, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8882008782994995, + "yMeters": 2.1325950429818517, + "headingRadians": 0.24999999999999983, + "unwrappedHeadingRadians": 0.24999999999999983, + "arcLengthMeters": 1.0999999999999996, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.864010719577458, + "yMeters": 2.12628389652344, + "headingRadians": 0.26041666666666652, + "unwrappedHeadingRadians": 0.26041666666666652, + "arcLengthMeters": 1.1249999999999998, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8398876131637074, + "yMeters": 2.1197211162004397, + "headingRadians": 0.2708333333333332, + "unwrappedHeadingRadians": 0.2708333333333332, + "arcLengthMeters": 1.15, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8158341765591473, + "yMeters": 2.1129074141136526, + "headingRadians": 0.28124999999999989, + "unwrappedHeadingRadians": 0.28124999999999989, + "arcLengthMeters": 1.175, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7918530197050868, + "yMeters": 2.1058435295903868, + "headingRadians": 0.29166666666666657, + "unwrappedHeadingRadians": 0.29166666666666657, + "arcLengthMeters": 1.2000000000000002, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7679467447000516, + "yMeters": 2.0985302291042367, + "headingRadians": 0.30208333333333326, + "unwrappedHeadingRadians": 0.30208333333333326, + "arcLengthMeters": 1.2250000000000003, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7441179455174403, + "yMeters": 2.0909683061919173, + "headingRadians": 0.31249999999999994, + "unwrappedHeadingRadians": 0.31249999999999994, + "arcLengthMeters": 1.2500000000000004, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7203692077240622, + "yMeters": 2.083158581367158, + "headingRadians": 0.32291666666666663, + "unwrappedHeadingRadians": 0.32291666666666663, + "arcLengthMeters": 1.2750000000000006, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6967031081995889, + "yMeters": 2.0751019020316743, + "headingRadians": 0.33333333333333331, + "unwrappedHeadingRadians": 0.33333333333333331, + "arcLengthMeters": 1.3000000000000007, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6731222148569469, + "yMeters": 2.0667991423832186, + "headingRadians": 0.34375, + "unwrappedHeadingRadians": 0.34375, + "arcLengthMeters": 1.3250000000000008, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6496290863636838, + "yMeters": 2.0582512033207241, + "headingRadians": 0.35416666666666669, + "unwrappedHeadingRadians": 0.35416666666666669, + "arcLengthMeters": 1.350000000000001, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6262262718643377, + "yMeters": 2.0494590123465537, + "headingRadians": 0.36458333333333337, + "unwrappedHeadingRadians": 0.36458333333333337, + "arcLengthMeters": 1.3750000000000011, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.60291631070384, + "yMeters": 2.0404235234658583, + "headingRadians": 0.37500000000000006, + "unwrappedHeadingRadians": 0.37500000000000006, + "arcLengthMeters": 1.4000000000000012, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5797017321519808, + "yMeters": 2.0311457170830627, + "headingRadians": 0.38541666666666674, + "unwrappedHeadingRadians": 0.38541666666666674, + "arcLengthMeters": 1.4250000000000014, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5565850551289686, + "yMeters": 2.0216265998954857, + "headingRadians": 0.39583333333333343, + "unwrappedHeadingRadians": 0.39583333333333343, + "arcLengthMeters": 1.4500000000000015, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.533568787932112, + "yMeters": 2.011867204784108, + "headingRadians": 0.40625000000000011, + "unwrappedHeadingRadians": 0.40625000000000011, + "arcLengthMeters": 1.4750000000000016, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5106554279636548, + "yMeters": 2.001868590701497, + "headingRadians": 0.41666666666666674, + "unwrappedHeadingRadians": 0.41666666666666674, + "arcLengthMeters": 1.5000000000000016, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4879013955922789, + "yMeters": 1.9915133309000244, + "headingRadians": 0.43750000000000006, + "unwrappedHeadingRadians": 0.43750000000000006, + "arcLengthMeters": 1.5250000000000015, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.465368019957094, + "yMeters": 1.9806863102019154, + "headingRadians": 0.45833333333333337, + "unwrappedHeadingRadians": 0.45833333333333337, + "arcLengthMeters": 1.5500000000000014, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4430650808153214, + "yMeters": 1.96939222766494, + "headingRadians": 0.47916666666666669, + "unwrappedHeadingRadians": 0.47916666666666669, + "arcLengthMeters": 1.5750000000000013, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4210022579119608, + "yMeters": 1.9576359850573479, + "headingRadians": 0.5, + "unwrappedHeadingRadians": 0.5, + "arcLengthMeters": 1.6000000000000012, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3991891267786634, + "yMeters": 1.9454226847304421, + "headingRadians": 0.52083333333333337, + "unwrappedHeadingRadians": 0.52083333333333337, + "arcLengthMeters": 1.6250000000000013, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3776351545778365, + "yMeters": 1.932757627404097, + "headingRadians": 0.54166666666666674, + "unwrappedHeadingRadians": 0.54166666666666674, + "arcLengthMeters": 1.6500000000000015, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.35634969599378, + "yMeters": 1.919646309866182, + "headingRadians": 0.56250000000000011, + "unwrappedHeadingRadians": 0.56250000000000011, + "arcLengthMeters": 1.6750000000000016, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3353419891726404, + "yMeters": 1.9060944225868888, + "headingRadians": 0.58333333333333348, + "unwrappedHeadingRadians": 0.58333333333333348, + "arcLengthMeters": 1.7000000000000017, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.314621151712944, + "yMeters": 1.8921078472489987, + "headingRadians": 0.60416666666666685, + "unwrappedHeadingRadians": 0.60416666666666685, + "arcLengthMeters": 1.7250000000000019, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2941961767084493, + "yMeters": 1.8776926541951617, + "headingRadians": 0.62500000000000022, + "unwrappedHeadingRadians": 0.62500000000000022, + "arcLengthMeters": 1.750000000000002, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.274075928845035, + "yMeters": 1.8628550997932936, + "headingRadians": 0.64583333333333359, + "unwrappedHeadingRadians": 0.64583333333333359, + "arcLengthMeters": 1.7750000000000021, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2542691405533195, + "yMeters": 1.8476016237212376, + "headingRadians": 0.666666666666667, + "unwrappedHeadingRadians": 0.666666666666667, + "arcLengthMeters": 1.8000000000000023, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2347844082186807, + "yMeters": 1.8319388461718658, + "headingRadians": 0.68750000000000033, + "unwrappedHeadingRadians": 0.68750000000000033, + "arcLengthMeters": 1.8250000000000024, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2156301884503209, + "yMeters": 1.8158735649798357, + "headingRadians": 0.7083333333333337, + "unwrappedHeadingRadians": 0.7083333333333337, + "arcLengthMeters": 1.8500000000000025, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1968147944109959, + "yMeters": 1.7994127526712478, + "headingRadians": 0.72916666666666707, + "unwrappedHeadingRadians": 0.72916666666666707, + "arcLengthMeters": 1.8750000000000027, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1783463922090027, + "yMeters": 1.7825635534374848, + "headingRadians": 0.75000000000000044, + "unwrappedHeadingRadians": 0.75000000000000044, + "arcLengthMeters": 1.9000000000000028, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1602329973539887, + "yMeters": 1.7653332800345474, + "headingRadians": 0.77083333333333381, + "unwrappedHeadingRadians": 0.77083333333333381, + "arcLengthMeters": 1.9250000000000029, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.142482471278125, + "yMeters": 1.7477294106092278, + "headingRadians": 0.79166666666666718, + "unwrappedHeadingRadians": 0.79166666666666718, + "arcLengthMeters": 1.9500000000000031, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1251025179241485, + "yMeters": 1.7297595854535055, + "headingRadians": 0.81250000000000056, + "unwrappedHeadingRadians": 0.81250000000000056, + "arcLengthMeters": 1.9750000000000032, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.108100680401759, + "yMeters": 1.7114316036885677, + "headingRadians": 0.83333333333333381, + "unwrappedHeadingRadians": 0.83333333333333381, + "arcLengthMeters": 2.0000000000000031, + "direction": 1, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0913870546268221, + "yMeters": 1.6928399641139515, + "headingRadians": 0.84375000000000044, + "unwrappedHeadingRadians": 0.84375000000000044, + "arcLengthMeters": 2.025000000000003, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0748679950258768, + "yMeters": 1.6740752360712245, + "headingRadians": 0.85416666666666707, + "unwrappedHeadingRadians": 0.85416666666666707, + "arcLengthMeters": 2.0500000000000029, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0585452940153983, + "yMeters": 1.6551394556452796, + "headingRadians": 0.8645833333333337, + "unwrappedHeadingRadians": 0.8645833333333337, + "arcLengthMeters": 2.0750000000000028, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0424207227057833, + "yMeters": 1.6360346774812122, + "headingRadians": 0.87500000000000033, + "unwrappedHeadingRadians": 0.87500000000000033, + "arcLengthMeters": 2.1000000000000028, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0264960307091748, + "yMeters": 1.616762974561381, + "headingRadians": 0.885416666666667, + "unwrappedHeadingRadians": 0.885416666666667, + "arcLengthMeters": 2.1250000000000027, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0107729459496182, + "yMeters": 1.5973264379804757, + "headingRadians": 0.89583333333333359, + "unwrappedHeadingRadians": 0.89583333333333359, + "arcLengthMeters": 2.1500000000000026, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.99525317447557116, + "yMeters": 1.5777271767186209, + "headingRadians": 0.90625000000000022, + "unwrappedHeadingRadians": 0.90625000000000022, + "arcLengthMeters": 2.1750000000000025, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.97993840027478751, + "yMeters": 1.5579673174125404, + "headingRadians": 0.91666666666666685, + "unwrappedHeadingRadians": 0.91666666666666685, + "arcLengthMeters": 2.2000000000000024, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.9648302850915953, + "yMeters": 1.5380490041248025, + "headingRadians": 0.92708333333333348, + "unwrappedHeadingRadians": 0.92708333333333348, + "arcLengthMeters": 2.2250000000000023, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.94993046824658556, + "yMeters": 1.517974398111178, + "headingRadians": 0.93750000000000011, + "unwrappedHeadingRadians": 0.93750000000000011, + "arcLengthMeters": 2.2500000000000022, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.93524056645873832, + "yMeters": 1.4977456775861304, + "headingRadians": 0.94791666666666674, + "unwrappedHeadingRadians": 0.94791666666666674, + "arcLengthMeters": 2.2750000000000021, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.92076217366999757, + "yMeters": 1.4773650374864664, + "headingRadians": 0.95833333333333337, + "unwrappedHeadingRadians": 0.95833333333333337, + "arcLengthMeters": 2.300000000000002, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.90649686087232073, + "yMeters": 1.4568346892331732, + "headingRadians": 0.96875, + "unwrappedHeadingRadians": 0.96875, + "arcLengthMeters": 2.325000000000002, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.89244617593721387, + "yMeters": 1.4361568604914643, + "headingRadians": 0.97916666666666663, + "unwrappedHeadingRadians": 0.97916666666666663, + "arcLengthMeters": 2.3500000000000019, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.87861164344778131, + "yMeters": 1.4153337949290665, + "headingRadians": 0.98958333333333326, + "unwrappedHeadingRadians": 0.98958333333333326, + "arcLengthMeters": 2.3750000000000018, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.86499476453329749, + "yMeters": 1.3943677519727675, + "headingRadians": 0.99999999999999989, + "unwrappedHeadingRadians": 0.99999999999999989, + "arcLengthMeters": 2.4000000000000017, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.85159701670632626, + "yMeters": 1.3732610065632553, + "headingRadians": 1.0104166666666665, + "unwrappedHeadingRadians": 1.0104166666666665, + "arcLengthMeters": 2.4250000000000016, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.83841985370240135, + "yMeters": 1.3520158489082721, + "headingRadians": 1.0208333333333333, + "unwrappedHeadingRadians": 1.0208333333333333, + "arcLengthMeters": 2.450000000000002, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.82546470532228866, + "yMeters": 1.3306345842341154, + "headingRadians": 1.03125, + "unwrappedHeadingRadians": 1.03125, + "arcLengthMeters": 2.4750000000000023, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.2092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.81273297727684268, + "yMeters": 1.3091195325355052, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.5000000000000018, + "direction": 1, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 1.2092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.81273297727684268, + "yMeters": 1.3091195325355052, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.5000000000000018, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.2092016093317763, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 0.82535253263774133, + "yMeters": 1.3307007007729572, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.5250000000000017, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.2092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.83797208799864, + "yMeters": 1.3522818690104093, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.5500000000000016, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.2092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.85059164335953863, + "yMeters": 1.3738630372478613, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.5750000000000015, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.86321119872043728, + "yMeters": 1.3954442054853133, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.6000000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.87583075408133593, + "yMeters": 1.4170253737227654, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.6250000000000013, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.88845030944223458, + "yMeters": 1.4386065419602174, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.6500000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1592016093317761, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.90106986480313322, + "yMeters": 1.4601877101976695, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.6750000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.91368942016403187, + "yMeters": 1.4817688784351215, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.7000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.92630897552493052, + "yMeters": 1.5033500466725735, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.725000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.93892853088582917, + "yMeters": 1.5249312149100256, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.7500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.1092016093317763, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.95154808624672782, + "yMeters": 1.5465123831474776, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.7750000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.96416764160762647, + "yMeters": 1.5680935513849297, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.8000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.97678719696852512, + "yMeters": 1.5896747196223817, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.8250000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.98940675232942377, + "yMeters": 1.6112558878598338, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.8500000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0592016093317764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0020263076903224, + "yMeters": 1.6328370560972858, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.8750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0146458630512212, + "yMeters": 1.6544182243347378, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.9000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.02726541841212, + "yMeters": 1.6759993925721899, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.9250000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0398849737730187, + "yMeters": 1.6975805608096419, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.95, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 1.0092016093317762, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0525045291339175, + "yMeters": 1.719161729047094, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 2.975, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.065124084494816, + "yMeters": 1.740742897284546, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0777436398557148, + "yMeters": 1.7623240655219981, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0903631952166135, + "yMeters": 1.78390523375945, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.05, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.95920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1029827505775123, + "yMeters": 1.8054864019969021, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.0749999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.115602305938411, + "yMeters": 1.8270675702343542, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.0999999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1282218612993098, + "yMeters": 1.8486487384718062, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.1249999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1408414166602086, + "yMeters": 1.8702299067092583, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.1499999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.90920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1534609720211073, + "yMeters": 1.8918110749467103, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.1749999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1660805273820061, + "yMeters": 1.9133922431841623, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.1999999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1787000827429048, + "yMeters": 1.9349734114216144, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.2249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1913196381038036, + "yMeters": 1.9565545796590664, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.2499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.85920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2039391934647024, + "yMeters": 1.9781357478965185, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.274999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2165587488256011, + "yMeters": 1.9997169161339705, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.2999999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2291783041864999, + "yMeters": 2.0212980843714226, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.3249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2417978595473986, + "yMeters": 2.0428792526088748, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.3499999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.80920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2544174149082974, + "yMeters": 2.0644604208463271, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.3749999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2670369702691961, + "yMeters": 2.0860415890837793, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.4, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2796565256300949, + "yMeters": 2.1076227573212316, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.4250000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2922760809909937, + "yMeters": 2.1292039255586839, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.4500000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.75920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3048956363518924, + "yMeters": 2.1507850937961361, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.475000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.317515191712791, + "yMeters": 2.172366262033588, + "headingRadians": 1.0416666666666665, + "unwrappedHeadingRadians": 1.0416666666666665, + "arcLengthMeters": 3.5000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3302469197582372, + "yMeters": 2.1938813137321986, + "headingRadians": 1.0312499999999998, + "unwrappedHeadingRadians": 1.0312499999999998, + "arcLengthMeters": 3.5250000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3432020681383499, + "yMeters": 2.2152625784063558, + "headingRadians": 1.020833333333333, + "unwrappedHeadingRadians": 1.020833333333333, + "arcLengthMeters": 3.5500000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.70920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3563792311422747, + "yMeters": 2.236507736061339, + "headingRadians": 1.0104166666666663, + "unwrappedHeadingRadians": 1.0104166666666663, + "arcLengthMeters": 3.5750000000000015, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.369776978969246, + "yMeters": 2.2576144814708514, + "headingRadians": 0.99999999999999967, + "unwrappedHeadingRadians": 0.99999999999999967, + "arcLengthMeters": 3.6000000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3833938578837297, + "yMeters": 2.2785805244271504, + "headingRadians": 0.989583333333333, + "unwrappedHeadingRadians": 0.989583333333333, + "arcLengthMeters": 3.6250000000000013, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3972283903731624, + "yMeters": 2.2994035899895477, + "headingRadians": 0.97916666666666641, + "unwrappedHeadingRadians": 0.97916666666666641, + "arcLengthMeters": 3.6500000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.65920160933177618, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4112790753082693, + "yMeters": 2.3200814187312564, + "headingRadians": 0.96874999999999978, + "unwrappedHeadingRadians": 0.96874999999999978, + "arcLengthMeters": 3.6750000000000012, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4255443881059464, + "yMeters": 2.34061176698455, + "headingRadians": 0.95833333333333315, + "unwrappedHeadingRadians": 0.95833333333333315, + "arcLengthMeters": 3.7000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4400227808946868, + "yMeters": 2.3609924070842139, + "headingRadians": 0.94791666666666652, + "unwrappedHeadingRadians": 0.94791666666666652, + "arcLengthMeters": 3.725000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.60920160933177614, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4547126826825341, + "yMeters": 2.3812211276092614, + "headingRadians": 0.93749999999999989, + "unwrappedHeadingRadians": 0.93749999999999989, + "arcLengthMeters": 3.7500000000000009, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4696124995275437, + "yMeters": 2.4012957336228857, + "headingRadians": 0.92708333333333326, + "unwrappedHeadingRadians": 0.92708333333333326, + "arcLengthMeters": 3.7750000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4847206147107361, + "yMeters": 2.4212140469106234, + "headingRadians": 0.91666666666666663, + "unwrappedHeadingRadians": 0.91666666666666663, + "arcLengthMeters": 3.8000000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.55920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5000353889115194, + "yMeters": 2.440973906216704, + "headingRadians": 0.90625, + "unwrappedHeadingRadians": 0.90625, + "arcLengthMeters": 3.8250000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5155551603855666, + "yMeters": 2.4605731674785587, + "headingRadians": 0.89583333333333337, + "unwrappedHeadingRadians": 0.89583333333333337, + "arcLengthMeters": 3.8500000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5312782451451232, + "yMeters": 2.4800097040594644, + "headingRadians": 0.88541666666666674, + "unwrappedHeadingRadians": 0.88541666666666674, + "arcLengthMeters": 3.8750000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.547202937141732, + "yMeters": 2.4992814069792955, + "headingRadians": 0.87500000000000011, + "unwrappedHeadingRadians": 0.87500000000000011, + "arcLengthMeters": 3.9000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5633275084513467, + "yMeters": 2.5183861851433629, + "headingRadians": 0.86458333333333348, + "unwrappedHeadingRadians": 0.86458333333333348, + "arcLengthMeters": 3.9250000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5796502094618252, + "yMeters": 2.537321965569308, + "headingRadians": 0.85416666666666685, + "unwrappedHeadingRadians": 0.85416666666666685, + "arcLengthMeters": 3.95, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5961692690627705, + "yMeters": 2.5560866936120346, + "headingRadians": 0.84375000000000022, + "unwrappedHeadingRadians": 0.84375000000000022, + "arcLengthMeters": 3.975, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6128828948377079, + "yMeters": 2.5746783331866507, + "headingRadians": 0.83333333333333359, + "unwrappedHeadingRadians": 0.83333333333333359, + "arcLengthMeters": 4.0, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6297892732585719, + "yMeters": 2.5930948669893947, + "headingRadians": 0.822916666666667, + "unwrappedHeadingRadians": 0.822916666666667, + "arcLengthMeters": 4.025, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6468865698824864, + "yMeters": 2.6113342967165258, + "headingRadians": 0.81250000000000033, + "unwrappedHeadingRadians": 0.81250000000000033, + "arcLengthMeters": 4.0500000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6641729295508114, + "yMeters": 2.6293946432811515, + "headingRadians": 0.8020833333333337, + "unwrappedHeadingRadians": 0.8020833333333337, + "arcLengthMeters": 4.0750000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6816464765904391, + "yMeters": 2.64727394702797, + "headingRadians": 0.79166666666666707, + "unwrappedHeadingRadians": 0.79166666666666707, + "arcLengthMeters": 4.1000000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6993053150173156, + "yMeters": 2.6649702679459053, + "headingRadians": 0.78125000000000044, + "unwrappedHeadingRadians": 0.78125000000000044, + "arcLengthMeters": 4.1250000000000018, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7171475287421665, + "yMeters": 2.6824816858786091, + "headingRadians": 0.77083333333333381, + "unwrappedHeadingRadians": 0.77083333333333381, + "arcLengthMeters": 4.1500000000000021, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7351711817784043, + "yMeters": 2.69980630073281, + "headingRadians": 0.76041666666666718, + "unwrappedHeadingRadians": 0.76041666666666718, + "arcLengthMeters": 4.1750000000000025, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7533743184521942, + "yMeters": 2.7169422326844841, + "headingRadians": 0.75000000000000056, + "unwrappedHeadingRadians": 0.75000000000000056, + "arcLengthMeters": 4.2000000000000028, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7717549636146559, + "yMeters": 2.7338876223828286, + "headingRadians": 0.73958333333333393, + "unwrappedHeadingRadians": 0.73958333333333393, + "arcLengthMeters": 4.2250000000000032, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7903111228561805, + "yMeters": 2.75064063115201, + "headingRadians": 0.7291666666666673, + "unwrappedHeadingRadians": 0.7291666666666673, + "arcLengthMeters": 4.2500000000000036, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8090407827228339, + "yMeters": 2.767199441190674, + "headingRadians": 0.71875000000000067, + "unwrappedHeadingRadians": 0.71875000000000067, + "arcLengthMeters": 4.2750000000000039, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8279419109348298, + "yMeters": 2.7835622557691857, + "headingRadians": 0.708333333333334, + "unwrappedHeadingRadians": 0.708333333333334, + "arcLengthMeters": 4.3000000000000043, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8470124566070447, + "yMeters": 2.7997272994245876, + "headingRadians": 0.69791666666666741, + "unwrappedHeadingRadians": 0.69791666666666741, + "arcLengthMeters": 4.3250000000000046, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8662503504715493, + "yMeters": 2.8156928181532459, + "headingRadians": 0.68750000000000078, + "unwrappedHeadingRadians": 0.68750000000000078, + "arcLengthMeters": 4.350000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8856535051021381, + "yMeters": 2.8314570796011713, + "headingRadians": 0.67708333333333415, + "unwrappedHeadingRadians": 0.67708333333333415, + "arcLengthMeters": 4.3750000000000053, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9052198151408268, + "yMeters": 2.8470183732519896, + "headingRadians": 0.66666666666666752, + "unwrappedHeadingRadians": 0.66666666666666752, + "arcLengthMeters": 4.4000000000000057, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9249471575262962, + "yMeters": 2.862375010612543, + "headingRadians": 0.65625000000000089, + "unwrappedHeadingRadians": 0.65625000000000089, + "arcLengthMeters": 4.425000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9448333917242575, + "yMeters": 2.8775253253961015, + "headingRadians": 0.64583333333333426, + "unwrappedHeadingRadians": 0.64583333333333426, + "arcLengthMeters": 4.4500000000000064, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.1108664282504217, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9648763599597128, + "yMeters": 2.8924676737031652, + "headingRadians": 0.63541666666666763, + "unwrappedHeadingRadians": 0.63541666666666763, + "arcLengthMeters": 4.4750000000000068, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0609850517126862, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.985073887451086, + "yMeters": 2.907200434199837, + "headingRadians": 0.625000000000001, + "unwrappedHeadingRadians": 0.625000000000001, + "arcLengthMeters": 4.5000000000000071, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0609850517126862, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0054988624555805, + "yMeters": 2.921615627253674, + "headingRadians": 0.60416666666666763, + "unwrappedHeadingRadians": 0.60416666666666763, + "arcLengthMeters": 4.5250000000000075, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0262196999152766, + "yMeters": 2.9356022025915638, + "headingRadians": 0.58333333333333426, + "unwrappedHeadingRadians": 0.58333333333333426, + "arcLengthMeters": 4.5500000000000078, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0472274067364165, + "yMeters": 2.949154089870857, + "headingRadians": 0.56250000000000089, + "unwrappedHeadingRadians": 0.56250000000000089, + "arcLengthMeters": 4.5750000000000082, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0685128653204732, + "yMeters": 2.962265407408772, + "headingRadians": 0.54166666666666752, + "unwrappedHeadingRadians": 0.54166666666666752, + "arcLengthMeters": 4.6000000000000085, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0900668375213, + "yMeters": 2.9749304647351171, + "headingRadians": 0.52083333333333415, + "unwrappedHeadingRadians": 0.52083333333333415, + "arcLengthMeters": 4.6250000000000089, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1118799686545975, + "yMeters": 2.9871437650620232, + "headingRadians": 0.50000000000000078, + "unwrappedHeadingRadians": 0.50000000000000078, + "arcLengthMeters": 4.6500000000000092, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1339427915579581, + "yMeters": 2.9989000076696155, + "headingRadians": 0.47916666666666746, + "unwrappedHeadingRadians": 0.47916666666666746, + "arcLengthMeters": 4.67500000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1562457306997307, + "yMeters": 3.0101940902065909, + "headingRadians": 0.45833333333333415, + "unwrappedHeadingRadians": 0.45833333333333415, + "arcLengthMeters": 4.70000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1787791063349156, + "yMeters": 3.0210211109047, + "headingRadians": 0.43750000000000083, + "unwrappedHeadingRadians": 0.43750000000000083, + "arcLengthMeters": 4.72500000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2015331387062913, + "yMeters": 3.0313763707061723, + "headingRadians": 0.41666666666666752, + "unwrappedHeadingRadians": 0.41666666666666752, + "arcLengthMeters": 4.7500000000000107, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2244979522889481, + "yMeters": 3.0412553753031668, + "headingRadians": 0.3958333333333342, + "unwrappedHeadingRadians": 0.3958333333333342, + "arcLengthMeters": 4.775000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2476635800763836, + "yMeters": 3.0506538370883529, + "headingRadians": 0.37500000000000089, + "unwrappedHeadingRadians": 0.37500000000000089, + "arcLengthMeters": 4.8000000000000114, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2710199679063052, + "yMeters": 3.059567677015786, + "headingRadians": 0.35416666666666757, + "unwrappedHeadingRadians": 0.35416666666666757, + "arcLengthMeters": 4.8250000000000117, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2945569788242577, + "yMeters": 3.0679930263712611, + "headingRadians": 0.33333333333333426, + "unwrappedHeadingRadians": 0.33333333333333426, + "arcLengthMeters": 4.8500000000000121, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3182643974831834, + "yMeters": 3.0759262284513826, + "headingRadians": 0.31250000000000094, + "unwrappedHeadingRadians": 0.31250000000000094, + "arcLengthMeters": 4.8750000000000124, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3421319345770066, + "yMeters": 3.0833638401506174, + "headingRadians": 0.29166666666666763, + "unwrappedHeadingRadians": 0.29166666666666763, + "arcLengthMeters": 4.9000000000000128, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3661492313063168, + "yMeters": 3.0903026334556438, + "headingRadians": 0.27083333333333431, + "unwrappedHeadingRadians": 0.27083333333333431, + "arcLengthMeters": 4.9250000000000131, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3903058638742127, + "yMeters": 3.0967395968463496, + "headingRadians": 0.250000000000001, + "unwrappedHeadingRadians": 0.250000000000001, + "arcLengthMeters": 4.9500000000000135, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4145913480103558, + "yMeters": 3.1026719366028668, + "headingRadians": 0.22916666666666766, + "unwrappedHeadingRadians": 0.22916666666666766, + "arcLengthMeters": 4.9750000000000139, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4389951435212707, + "yMeters": 3.1080970780180803, + "headingRadians": 0.20833333333333445, + "unwrappedHeadingRadians": 0.20833333333333445, + "arcLengthMeters": 5.0000000000000133, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4635066588649162, + "yMeters": 3.1130126665150821, + "headingRadians": 0.18750000000000111, + "unwrappedHeadingRadians": 0.18750000000000111, + "arcLengthMeters": 5.0250000000000137, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4881152557475419, + "yMeters": 3.117416568669086, + "headingRadians": 0.16666666666666777, + "unwrappedHeadingRadians": 0.16666666666666777, + "arcLengthMeters": 5.050000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5128102537408377, + "yMeters": 3.1213068731333591, + "headingRadians": 0.14583333333333443, + "unwrappedHeadingRadians": 0.14583333333333443, + "arcLengthMeters": 5.0750000000000144, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5375809349173668, + "yMeters": 3.1246818914687706, + "headingRadians": 0.12500000000000108, + "unwrappedHeadingRadians": 0.12500000000000108, + "arcLengthMeters": 5.1000000000000147, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5624165485022772, + "yMeters": 3.1275401588765939, + "headingRadians": 0.10416666666666774, + "unwrappedHeadingRadians": 0.10416666666666774, + "arcLengthMeters": 5.1250000000000151, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5873063155392675, + "yMeters": 3.1298804348342468, + "headingRadians": 0.0833333333333344, + "unwrappedHeadingRadians": 0.0833333333333344, + "arcLengthMeters": 5.1500000000000155, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6122394335687837, + "yMeters": 3.1317017036336945, + "headingRadians": 0.062500000000001055, + "unwrappedHeadingRadians": 0.062500000000001055, + "arcLengthMeters": 5.1750000000000158, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.637205081316417, + "yMeters": 3.1330031748222771, + "headingRadians": 0.041666666666667719, + "unwrappedHeadingRadians": 0.041666666666667719, + "arcLengthMeters": 5.2000000000000162, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6621924233894685, + "yMeters": 3.1337842835457779, + "headingRadians": 0.020833333333334383, + "unwrappedHeadingRadians": 0.020833333333334383, + "arcLengthMeters": 5.2250000000000165, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.68719061497964, + "yMeters": 3.1340446907935751, + "headingRadians": 1.0477729794899915E-15, + "unwrappedHeadingRadians": 1.0477729794899915E-15, + "arcLengthMeters": 5.2500000000000169, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7121888065698112, + "yMeters": 3.1337842835457779, + "headingRadians": -0.020833333333332288, + "unwrappedHeadingRadians": -0.020833333333332288, + "arcLengthMeters": 5.2750000000000172, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7371761486428627, + "yMeters": 3.1330031748222771, + "headingRadians": -0.041666666666665624, + "unwrappedHeadingRadians": -0.041666666666665624, + "arcLengthMeters": 5.3000000000000176, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.762141796390496, + "yMeters": 3.131701703633694, + "headingRadians": -0.062499999999998959, + "unwrappedHeadingRadians": -0.062499999999998959, + "arcLengthMeters": 5.3250000000000179, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7870749144200122, + "yMeters": 3.1298804348342464, + "headingRadians": -0.083333333333332288, + "unwrappedHeadingRadians": -0.083333333333332288, + "arcLengthMeters": 5.3500000000000183, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8119646814570025, + "yMeters": 3.1275401588765934, + "headingRadians": -0.10416666666666563, + "unwrappedHeadingRadians": -0.10416666666666563, + "arcLengthMeters": 5.3750000000000187, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8368002950419129, + "yMeters": 3.12468189146877, + "headingRadians": -0.12499999999999897, + "unwrappedHeadingRadians": -0.12499999999999897, + "arcLengthMeters": 5.400000000000019, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.861570976218442, + "yMeters": 3.1213068731333586, + "headingRadians": -0.14583333333333232, + "unwrappedHeadingRadians": -0.14583333333333232, + "arcLengthMeters": 5.4250000000000194, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8862659742117378, + "yMeters": 3.1174165686690856, + "headingRadians": -0.16666666666666566, + "unwrappedHeadingRadians": -0.16666666666666566, + "arcLengthMeters": 5.45000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9108745710943635, + "yMeters": 3.1130126665150817, + "headingRadians": -0.187499999999999, + "unwrappedHeadingRadians": -0.187499999999999, + "arcLengthMeters": 5.47500000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9353860864380086, + "yMeters": 3.10809707801808, + "headingRadians": -0.2083333333333322, + "unwrappedHeadingRadians": -0.2083333333333322, + "arcLengthMeters": 5.5000000000000195, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9598181381492945, + "yMeters": 3.1027990408537955, + "headingRadians": -0.21874999999999886, + "unwrappedHeadingRadians": -0.21874999999999886, + "arcLengthMeters": 5.52500000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9841936774598388, + "yMeters": 3.0972467951876528, + "headingRadians": -0.22916666666666552, + "unwrappedHeadingRadians": -0.22916666666666552, + "arcLengthMeters": 5.55000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0085100594782674, + "yMeters": 3.0914409434714161, + "headingRadians": -0.23958333333333218, + "unwrappedHeadingRadians": -0.23958333333333218, + "arcLengthMeters": 5.5750000000000206, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0327646457321253, + "yMeters": 3.0853821156746184, + "headingRadians": -0.24999999999999883, + "unwrappedHeadingRadians": -0.24999999999999883, + "arcLengthMeters": 5.600000000000021, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.056954804454167, + "yMeters": 3.0790709692162066, + "headingRadians": -0.26041666666666552, + "unwrappedHeadingRadians": -0.26041666666666552, + "arcLengthMeters": 5.6250000000000213, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0810779108679176, + "yMeters": 3.0725081888932069, + "headingRadians": -0.2708333333333322, + "unwrappedHeadingRadians": -0.2708333333333322, + "arcLengthMeters": 5.6500000000000217, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.105131347472478, + "yMeters": 3.0656944868064198, + "headingRadians": -0.28124999999999889, + "unwrappedHeadingRadians": -0.28124999999999889, + "arcLengthMeters": 5.675000000000022, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1291125043265384, + "yMeters": 3.0586306022831535, + "headingRadians": -0.29166666666666557, + "unwrappedHeadingRadians": -0.29166666666666557, + "arcLengthMeters": 5.7000000000000224, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1530187793315738, + "yMeters": 3.0513173017970039, + "headingRadians": -0.30208333333333226, + "unwrappedHeadingRadians": -0.30208333333333226, + "arcLengthMeters": 5.7250000000000227, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1768475785141854, + "yMeters": 3.0437553788846845, + "headingRadians": -0.31249999999999895, + "unwrappedHeadingRadians": -0.31249999999999895, + "arcLengthMeters": 5.7500000000000231, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2005963163075637, + "yMeters": 3.0359456540599252, + "headingRadians": -0.32291666666666563, + "unwrappedHeadingRadians": -0.32291666666666563, + "arcLengthMeters": 5.7750000000000234, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2242624158320368, + "yMeters": 3.0278889747244415, + "headingRadians": -0.33333333333333232, + "unwrappedHeadingRadians": -0.33333333333333232, + "arcLengthMeters": 5.8000000000000238, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2478433091746788, + "yMeters": 3.0195862150759858, + "headingRadians": -0.343749999999999, + "unwrappedHeadingRadians": -0.343749999999999, + "arcLengthMeters": 5.8250000000000242, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2713364376679417, + "yMeters": 3.0110382760134913, + "headingRadians": -0.35416666666666569, + "unwrappedHeadingRadians": -0.35416666666666569, + "arcLengthMeters": 5.8500000000000245, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2947392521672878, + "yMeters": 3.0022460850393209, + "headingRadians": -0.36458333333333237, + "unwrappedHeadingRadians": -0.36458333333333237, + "arcLengthMeters": 5.8750000000000249, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3180492133277855, + "yMeters": 2.9932105961586255, + "headingRadians": -0.37499999999999906, + "unwrappedHeadingRadians": -0.37499999999999906, + "arcLengthMeters": 5.9000000000000252, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3412637918796446, + "yMeters": 2.98393278977583, + "headingRadians": -0.38541666666666574, + "unwrappedHeadingRadians": -0.38541666666666574, + "arcLengthMeters": 5.9250000000000256, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3643804689026569, + "yMeters": 2.9744136725882528, + "headingRadians": -0.39583333333333243, + "unwrappedHeadingRadians": -0.39583333333333243, + "arcLengthMeters": 5.9500000000000259, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3873967360995136, + "yMeters": 2.9646542774768752, + "headingRadians": -0.40624999999999911, + "unwrappedHeadingRadians": -0.40624999999999911, + "arcLengthMeters": 5.9750000000000263, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4103100960679704, + "yMeters": 2.9546556633942642, + "headingRadians": -0.41666666666666574, + "unwrappedHeadingRadians": -0.41666666666666574, + "arcLengthMeters": 6.0000000000000266, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4331180625718325, + "yMeters": 2.9444189152496723, + "headingRadians": -0.42708333333333243, + "unwrappedHeadingRadians": -0.42708333333333243, + "arcLengthMeters": 6.025000000000027, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4558181608107228, + "yMeters": 2.9339451437913189, + "headingRadians": -0.43749999999999911, + "unwrappedHeadingRadians": -0.43749999999999911, + "arcLengthMeters": 6.0500000000000274, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4784079276886146, + "yMeters": 2.9232354854858649, + "headingRadians": -0.4479166666666658, + "unwrappedHeadingRadians": -0.4479166666666658, + "arcLengthMeters": 6.0750000000000277, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5008849120810925, + "yMeters": 2.912291102395101, + "headingRadians": -0.45833333333333248, + "unwrappedHeadingRadians": -0.45833333333333248, + "arcLengthMeters": 6.1000000000000281, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5232466751013125, + "yMeters": 2.9011131820498579, + "headingRadians": -0.46874999999999917, + "unwrappedHeadingRadians": -0.46874999999999917, + "arcLengthMeters": 6.1250000000000284, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5454907903646373, + "yMeters": 2.8897029373211502, + "headingRadians": -0.47916666666666585, + "unwrappedHeadingRadians": -0.47916666666666585, + "arcLengthMeters": 6.1500000000000288, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5676148442519127, + "yMeters": 2.8780616062885733, + "headingRadians": -0.48958333333333254, + "unwrappedHeadingRadians": -0.48958333333333254, + "arcLengthMeters": 6.1750000000000291, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5896164361713589, + "yMeters": 2.8661904521059656, + "headingRadians": -0.49999999999999922, + "unwrappedHeadingRadians": -0.49999999999999922, + "arcLengthMeters": 6.2000000000000295, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6114931788190505, + "yMeters": 2.854090762864347, + "headingRadians": -0.51041666666666585, + "unwrappedHeadingRadians": -0.51041666666666585, + "arcLengthMeters": 6.22500000000003, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6332426984379533, + "yMeters": 2.8417638514521544, + "headingRadians": -0.52083333333333248, + "unwrappedHeadingRadians": -0.52083333333333248, + "arcLengthMeters": 6.25000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6548626350754891, + "yMeters": 2.8292110554127845, + "headingRadians": -0.53124999999999911, + "unwrappedHeadingRadians": -0.53124999999999911, + "arcLengthMeters": 6.2750000000000306, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6763506428396067, + "yMeters": 2.8164337367994645, + "headingRadians": -0.54166666666666574, + "unwrappedHeadingRadians": -0.54166666666666574, + "arcLengthMeters": 6.3000000000000309, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6977043901533242, + "yMeters": 2.8034332820274583, + "headingRadians": -0.55208333333333237, + "unwrappedHeadingRadians": -0.55208333333333237, + "arcLengthMeters": 6.3250000000000313, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7189215600077197, + "yMeters": 2.7902111017236346, + "headingRadians": -0.562499999999999, + "unwrappedHeadingRadians": -0.562499999999999, + "arcLengthMeters": 6.3500000000000316, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7399998502133389, + "yMeters": 2.7767686305734043, + "headingRadians": -0.57291666666666563, + "unwrappedHeadingRadians": -0.57291666666666563, + "arcLengthMeters": 6.375000000000032, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7609369736499989, + "yMeters": 2.7631073271650486, + "headingRadians": -0.58333333333333226, + "unwrappedHeadingRadians": -0.58333333333333226, + "arcLengthMeters": 6.4000000000000323, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7817306585149519, + "yMeters": 2.7492286738314538, + "headingRadians": -0.59374999999999889, + "unwrappedHeadingRadians": -0.59374999999999889, + "arcLengthMeters": 6.4250000000000327, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8023786485693911, + "yMeters": 2.7351341764892689, + "headingRadians": -0.60416666666666552, + "unwrappedHeadingRadians": -0.60416666666666552, + "arcLengthMeters": 6.450000000000033, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8228787033832656, + "yMeters": 2.7208253644755054, + "headingRadians": -0.61458333333333215, + "unwrappedHeadingRadians": -0.61458333333333215, + "arcLengthMeters": 6.4750000000000334, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8432285985783805, + "yMeters": 2.706303790381595, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.5000000000000338, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8635026765660108, + "yMeters": 2.6916763585580834, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.5250000000000332, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.883776754553641, + "yMeters": 2.6770489267345718, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.5500000000000327, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9040508325412713, + "yMeters": 2.6624214949110603, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.5750000000000322, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9243249105289015, + "yMeters": 2.6477940630875487, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.6000000000000316, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9445989885165318, + "yMeters": 2.6331666312640372, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.6250000000000311, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.964873066504162, + "yMeters": 2.6185391994405256, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.6500000000000306, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9851471444917923, + "yMeters": 2.603911767617014, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.67500000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0054212224794226, + "yMeters": 2.5892843357935025, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.7000000000000295, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0256953004670528, + "yMeters": 2.5746569039699909, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.725000000000029, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0459693784546831, + "yMeters": 2.5600294721464794, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.7500000000000284, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0662434564423133, + "yMeters": 2.5454020403229678, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.7750000000000279, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0865175344299436, + "yMeters": 2.5307746084994562, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.8000000000000274, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1067916124175738, + "yMeters": 2.5161471766759447, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.8250000000000268, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1270656904052041, + "yMeters": 2.5015197448524331, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.8500000000000263, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1473397683928344, + "yMeters": 2.4868923130289216, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.8750000000000258, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1676138463804646, + "yMeters": 2.47226488120541, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.9000000000000252, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1878879243680949, + "yMeters": 2.4576374493818984, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.9250000000000247, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2081620023557251, + "yMeters": 2.4430100175583869, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.9500000000000242, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2284360803433554, + "yMeters": 2.4283825857348753, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 6.9750000000000236, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2487101583309856, + "yMeters": 2.4137551539113637, + "headingRadians": -0.62499999999999878, + "unwrappedHeadingRadians": -0.62499999999999878, + "arcLengthMeters": 7.0000000000000231, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2690600535261005, + "yMeters": 2.3992335798174533, + "headingRadians": -0.61458333333333215, + "unwrappedHeadingRadians": -0.61458333333333215, + "arcLengthMeters": 7.0250000000000234, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.289560108339975, + "yMeters": 2.38492476780369, + "headingRadians": -0.60416666666666552, + "unwrappedHeadingRadians": -0.60416666666666552, + "arcLengthMeters": 7.0500000000000238, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3102080983944147, + "yMeters": 2.370830270461505, + "headingRadians": -0.59374999999999889, + "unwrappedHeadingRadians": -0.59374999999999889, + "arcLengthMeters": 7.0750000000000242, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3310017832593672, + "yMeters": 2.35695161712791, + "headingRadians": -0.58333333333333226, + "unwrappedHeadingRadians": -0.58333333333333226, + "arcLengthMeters": 7.1000000000000245, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3519389066960272, + "yMeters": 2.3432903137195544, + "headingRadians": -0.57291666666666563, + "unwrappedHeadingRadians": -0.57291666666666563, + "arcLengthMeters": 7.1250000000000249, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.373017196901646, + "yMeters": 2.3298478425693241, + "headingRadians": -0.562499999999999, + "unwrappedHeadingRadians": -0.562499999999999, + "arcLengthMeters": 7.1500000000000252, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3942343667560415, + "yMeters": 2.3166256622655004, + "headingRadians": -0.55208333333333237, + "unwrappedHeadingRadians": -0.55208333333333237, + "arcLengthMeters": 7.1750000000000256, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4155881140697586, + "yMeters": 2.3036252074934942, + "headingRadians": -0.54166666666666574, + "unwrappedHeadingRadians": -0.54166666666666574, + "arcLengthMeters": 7.2000000000000259, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4370761218338766, + "yMeters": 2.2908478888801742, + "headingRadians": -0.53124999999999911, + "unwrappedHeadingRadians": -0.53124999999999911, + "arcLengthMeters": 7.2250000000000263, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4586960584714124, + "yMeters": 2.2782950928408043, + "headingRadians": -0.52083333333333248, + "unwrappedHeadingRadians": -0.52083333333333248, + "arcLengthMeters": 7.2500000000000266, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4804455780903147, + "yMeters": 2.2659681814286117, + "headingRadians": -0.51041666666666585, + "unwrappedHeadingRadians": -0.51041666666666585, + "arcLengthMeters": 7.275000000000027, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5023223207380063, + "yMeters": 2.2538684921869931, + "headingRadians": -0.49999999999999917, + "unwrappedHeadingRadians": -0.49999999999999917, + "arcLengthMeters": 7.3000000000000274, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5243239126574526, + "yMeters": 2.2419973380043854, + "headingRadians": -0.48958333333333248, + "unwrappedHeadingRadians": -0.48958333333333248, + "arcLengthMeters": 7.3250000000000277, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5464479665447275, + "yMeters": 2.2303560069718089, + "headingRadians": -0.4791666666666658, + "unwrappedHeadingRadians": -0.4791666666666658, + "arcLengthMeters": 7.3500000000000281, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5686920818080523, + "yMeters": 2.2189457622431012, + "headingRadians": -0.46874999999999911, + "unwrappedHeadingRadians": -0.46874999999999911, + "arcLengthMeters": 7.3750000000000284, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5910538448282727, + "yMeters": 2.2077678418978581, + "headingRadians": -0.45833333333333243, + "unwrappedHeadingRadians": -0.45833333333333243, + "arcLengthMeters": 7.4000000000000288, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6135308292207506, + "yMeters": 2.1968234588070943, + "headingRadians": -0.44791666666666574, + "unwrappedHeadingRadians": -0.44791666666666574, + "arcLengthMeters": 7.4250000000000291, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6361205960986425, + "yMeters": 2.1861138005016403, + "headingRadians": -0.43749999999999906, + "unwrappedHeadingRadians": -0.43749999999999906, + "arcLengthMeters": 7.4500000000000295, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6588206943375328, + "yMeters": 2.1756400290432865, + "headingRadians": -0.42708333333333237, + "unwrappedHeadingRadians": -0.42708333333333237, + "arcLengthMeters": 7.47500000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6816286608413948, + "yMeters": 2.1654032808986949, + "headingRadians": -0.41666666666666574, + "unwrappedHeadingRadians": -0.41666666666666574, + "arcLengthMeters": 7.50000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7045934744240521, + "yMeters": 2.1555242763017004, + "headingRadians": -0.39583333333333243, + "unwrappedHeadingRadians": -0.39583333333333243, + "arcLengthMeters": 7.5250000000000306, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.727759102211488, + "yMeters": 2.1461258145165143, + "headingRadians": -0.37499999999999911, + "unwrappedHeadingRadians": -0.37499999999999911, + "arcLengthMeters": 7.5500000000000309, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.75111549004141, + "yMeters": 2.1372119745890812, + "headingRadians": -0.3541666666666658, + "unwrappedHeadingRadians": -0.3541666666666658, + "arcLengthMeters": 7.5750000000000313, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7746525009593626, + "yMeters": 2.1287866252336061, + "headingRadians": -0.33333333333333248, + "unwrappedHeadingRadians": -0.33333333333333248, + "arcLengthMeters": 7.6000000000000316, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7983599196182878, + "yMeters": 2.1208534231534846, + "headingRadians": -0.31249999999999917, + "unwrappedHeadingRadians": -0.31249999999999917, + "arcLengthMeters": 7.625000000000032, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8222274567121106, + "yMeters": 2.11341581145425, + "headingRadians": -0.29166666666666585, + "unwrappedHeadingRadians": -0.29166666666666585, + "arcLengthMeters": 7.6500000000000323, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8462447534414208, + "yMeters": 2.1064770181492234, + "headingRadians": -0.27083333333333254, + "unwrappedHeadingRadians": -0.27083333333333254, + "arcLengthMeters": 7.6750000000000327, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8704013860093172, + "yMeters": 2.1000400547585176, + "headingRadians": -0.2499999999999992, + "unwrappedHeadingRadians": -0.2499999999999992, + "arcLengthMeters": 7.700000000000033, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8946868701454607, + "yMeters": 2.0941077150020004, + "headingRadians": -0.22916666666666585, + "unwrappedHeadingRadians": -0.22916666666666585, + "arcLengthMeters": 7.7250000000000334, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.919090665656376, + "yMeters": 2.0886825735867869, + "headingRadians": -0.20833333333333251, + "unwrappedHeadingRadians": -0.20833333333333251, + "arcLengthMeters": 7.7500000000000338, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9436021810000215, + "yMeters": 2.0837669850897851, + "headingRadians": -0.18749999999999917, + "unwrappedHeadingRadians": -0.18749999999999917, + "arcLengthMeters": 7.7750000000000341, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9682107778826472, + "yMeters": 2.0793630829357812, + "headingRadians": -0.16666666666666582, + "unwrappedHeadingRadians": -0.16666666666666582, + "arcLengthMeters": 7.8000000000000345, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.9929057758759425, + "yMeters": 2.0754727784715081, + "headingRadians": -0.14583333333333248, + "unwrappedHeadingRadians": -0.14583333333333248, + "arcLengthMeters": 7.8250000000000348, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 5.0176764570524721, + "yMeters": 2.0720977601360966, + "headingRadians": -0.12499999999999914, + "unwrappedHeadingRadians": -0.12499999999999914, + "arcLengthMeters": 7.8500000000000352, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 5.0425120706373825, + "yMeters": 2.0692394927282733, + "headingRadians": -0.1041666666666658, + "unwrappedHeadingRadians": -0.1041666666666658, + "arcLengthMeters": 7.8750000000000355, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 5.0674018376743728, + "yMeters": 2.0668992167706204, + "headingRadians": -0.083333333333332454, + "unwrappedHeadingRadians": -0.083333333333332454, + "arcLengthMeters": 7.9000000000000359, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 40, + "startsAtGearSwitch": false, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 1, + "direction": 1, + "startIndex": 41, + "endIndex": 101, + "startsAtGearSwitch": true, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 2, + "direction": 0, + "startIndex": 102, + "endIndex": 318, + "startsAtGearSwitch": true, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "multi-obstacle-detour", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:d062cfcf4fec6c0119575bb0c139697d8e4d5164e958cf511c0200d0f8d9fb5d", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [ + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 2400.0, + "centerYMm": 1300.0, + "radiusMm": 220.0 + }, + { + "kind": "axis-aligned-rectangle", + "xMinMm": 3000.0, + "xMaxMm": 3600.0, + "yMinMm": 2000.0, + "yMaxMm": 2600.0, + "centerXMm": 0.0, + "centerYMm": 0.0, + "radiusMm": 0.0 + }, + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 3720.0, + "centerYMm": 2320.0, + "radiusMm": 140.0 + }, + { + "kind": "circle", + "xMinMm": 0.0, + "xMaxMm": 0.0, + "yMinMm": 0.0, + "yMaxMm": 0.0, + "centerXMm": 3720.0, + "centerYMm": 2680.0, + "radiusMm": 140.0 + } + ] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 1.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.0249995478901843, + "yMeters": 1.0001302071559617, + "headingRadians": 0.010416666666666666, + "unwrappedHeadingRadians": 0.010416666666666666, + "arcLengthMeters": 0.025, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499963831803427, + "yMeters": 1.0005208144955946, + "headingRadians": 0.020833333333333332, + "unwrappedHeadingRadians": 0.020833333333333332, + "arcLengthMeters": 0.05, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749877935647825, + "yMeters": 1.0011717796356725, + "headingRadians": 0.03125, + "unwrappedHeadingRadians": 0.03125, + "arcLengthMeters": 0.075000000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.47723559808167104, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999710673264458, + "yMeters": 1.0020830319425964, + "headingRadians": 0.041666666666666664, + "unwrappedHeadingRadians": 0.041666666666666664, + "arcLengthMeters": 0.1, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.47723559808167104, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249434936311464, + "yMeters": 1.0032544725400563, + "headingRadians": 0.052083333333333329, + "unwrappedHeadingRadians": 0.052083333333333329, + "arcLengthMeters": 0.125, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.42807952565337382, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499023628217124, + "yMeters": 1.004685974319762, + "headingRadians": 0.062499999999999993, + "unwrappedHeadingRadians": 0.062499999999999993, + "arcLengthMeters": 0.15, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.42807952565337382, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1748449667119993, + "yMeters": 1.0063773819552337, + "headingRadians": 0.072916666666666657, + "unwrappedHeadingRadians": 0.072916666666666657, + "arcLengthMeters": 0.175, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.37900551205033328, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1997685988807447, + "yMeters": 1.0083285119186571, + "headingRadians": 0.083333333333333329, + "unwrappedHeadingRadians": 0.083333333333333329, + "arcLengthMeters": 0.2, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.37900551205033328, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.224670554965231, + "yMeters": 1.0105391525007958, + "headingRadians": 0.09375, + "unwrappedHeadingRadians": 0.09375, + "arcLengthMeters": 0.22500000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.33002600127915627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2495481329547253, + "yMeters": 1.0130090638339635, + "headingRadians": 0.10416666666666667, + "unwrappedHeadingRadians": 0.10416666666666667, + "arcLengthMeters": 0.25000000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.33002600127915627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2743986334836632, + "yMeters": 1.0157379779180522, + "headingRadians": 0.11458333333333334, + "unwrappedHeadingRadians": 0.11458333333333334, + "arcLengthMeters": 0.27500000000000008, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.281156055061065, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2992193601245465, + "yMeters": 1.0187255986496104, + "headingRadians": 0.125, + "unwrappedHeadingRadians": 0.125, + "arcLengthMeters": 0.30000000000000004, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.281156055061065, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3240076196805208, + "yMeters": 1.0219716018539722, + "headingRadians": 0.13541666666666666, + "unwrappedHeadingRadians": 0.13541666666666666, + "arcLengthMeters": 0.325, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.23241406916042529, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3487607224776048, + "yMeters": 1.0254756353204335, + "headingRadians": 0.14583333333333331, + "unwrappedHeadingRadians": 0.14583333333333331, + "arcLengthMeters": 0.35, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.23241406916042529, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3734759826565346, + "yMeters": 1.0292373188404673, + "headingRadians": 0.15624999999999997, + "unwrappedHeadingRadians": 0.15624999999999997, + "arcLengthMeters": 0.37499999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3981507184641961, + "yMeters": 1.0332562442489797, + "headingRadians": 0.16666666666666663, + "unwrappedHeadingRadians": 0.16666666666666663, + "arcLengthMeters": 0.39999999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4227822525446114, + "yMeters": 1.0375319754685981, + "headingRadians": 0.17708333333333329, + "unwrappedHeadingRadians": 0.17708333333333329, + "arcLengthMeters": 0.42499999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4473679122294476, + "yMeters": 1.0420640485569872, + "headingRadians": 0.18749999999999994, + "unwrappedHeadingRadians": 0.18749999999999994, + "arcLengthMeters": 0.44999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4719050298280172, + "yMeters": 1.0468519717571914, + "headingRadians": 0.1979166666666666, + "unwrappedHeadingRadians": 0.1979166666666666, + "arcLengthMeters": 0.47499999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.087212598259828078, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963909429167384, + "yMeters": 1.051895225550991, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.49999999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.075092662495593987, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963909429167384, + "yMeters": 1.051895225550991, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.49999999999999967, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.075092662495593987, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 1.4719315181828945, + "yMeters": 1.0467244865622749, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.52499999999999969, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.075092662495593987, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4474720934490506, + "yMeters": 1.0415537475735588, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.54999999999999971, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.087212598259828078, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4230126687152067, + "yMeters": 1.0363830085848427, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.57499999999999973, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3985532439813628, + "yMeters": 1.0312122695961266, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.59999999999999976, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3740938192475189, + "yMeters": 1.0260415306074104, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.62499999999999978, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3496343945136751, + "yMeters": 1.0208707916186943, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.6499999999999998, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3251749697798312, + "yMeters": 1.0157000526299782, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.67499999999999982, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23241406916042529, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3007155450459873, + "yMeters": 1.0105293136412621, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.69999999999999984, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23241406916042529, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2762561203121434, + "yMeters": 1.005358574652546, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.72499999999999987, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23241406916042529, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2517966955782995, + "yMeters": 1.0001878356638298, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.74999999999999989, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.281156055061065, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2273372708444557, + "yMeters": 0.99501709667511384, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.77499999999999991, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.281156055061065, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2028778461106118, + "yMeters": 0.98984635768639784, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.79999999999999993, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.34154574455120124, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1784184213767679, + "yMeters": 0.98467561869768183, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.825, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.34154574455120124, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.153958996642924, + "yMeters": 0.97950487970896583, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.85, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.3899780157361914, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1294995719090801, + "yMeters": 0.97433414072024982, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.875, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.3899780157361914, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1050401471752362, + "yMeters": 0.96916340173153381, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.9, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.43855326657792137, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0805807224413924, + "yMeters": 0.96399266274281781, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.925, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.43855326657792137, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0561212977075485, + "yMeters": 0.9588219237541018, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.95000000000000007, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.48725302659962388, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0316618729737046, + "yMeters": 0.9536511847653858, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.97500000000000009, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.48725302659962388, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0072024482398609, + "yMeters": 0.94848044577666979, + "headingRadians": 0.2083333333333332, + "unwrappedHeadingRadians": 0.2083333333333332, + "arcLengthMeters": 0.99999999999999989, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.53606183892575821, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.98271653515113966, + "yMeters": 0.94343719198287013, + "headingRadians": 0.19791666666666655, + "unwrappedHeadingRadians": 0.19791666666666655, + "arcLengthMeters": 1.025, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.5476880417322475, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.95817941755257008, + "yMeters": 0.93864926878266608, + "headingRadians": 0.18749999999999989, + "unwrappedHeadingRadians": 0.18749999999999989, + "arcLengthMeters": 1.0499999999999998, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.5961332970170744, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.93359375786773391, + "yMeters": 0.934117195694277, + "headingRadians": 0.17708333333333323, + "unwrappedHeadingRadians": 0.17708333333333323, + "arcLengthMeters": 1.0749999999999997, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.5961332970170744, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.9089622237873185, + "yMeters": 0.92984146447465865, + "headingRadians": 0.16666666666666657, + "unwrappedHeadingRadians": 0.16666666666666657, + "arcLengthMeters": 1.0999999999999996, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.64469762255002039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.884287487979657, + "yMeters": 0.925822539066146, + "headingRadians": 0.15624999999999992, + "unwrappedHeadingRadians": 0.15624999999999992, + "arcLengthMeters": 1.1249999999999996, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.64469762255002039, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.85957222780072728, + "yMeters": 0.92206085554611239, + "headingRadians": 0.14583333333333326, + "unwrappedHeadingRadians": 0.14583333333333326, + "arcLengthMeters": 1.1499999999999995, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.69336801574440965, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.83481912500364341, + "yMeters": 0.91855682207965106, + "headingRadians": 0.1354166666666666, + "unwrappedHeadingRadians": 0.1354166666666666, + "arcLengthMeters": 1.1749999999999994, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.69336801574440965, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.810030865447669, + "yMeters": 0.915310818875289, + "headingRadians": 0.12499999999999993, + "unwrappedHeadingRadians": 0.12499999999999993, + "arcLengthMeters": 1.1999999999999993, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.74213327792570938, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.78521013880678581, + "yMeters": 0.91232319814373108, + "headingRadians": 0.11458333333333326, + "unwrappedHeadingRadians": 0.11458333333333326, + "arcLengthMeters": 1.2249999999999992, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.74213327792570938, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.7603596382778478, + "yMeters": 0.90959428405964238, + "headingRadians": 0.10416666666666659, + "unwrappedHeadingRadians": 0.10416666666666659, + "arcLengthMeters": 1.2499999999999991, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.79098371565941161, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.73548206028835339, + "yMeters": 0.90712437272647473, + "headingRadians": 0.093749999999999917, + "unwrappedHeadingRadians": 0.093749999999999917, + "arcLengthMeters": 1.274999999999999, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.79098371565941161, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.71058010420386708, + "yMeters": 0.90491373214433612, + "headingRadians": 0.083333333333333245, + "unwrappedHeadingRadians": 0.083333333333333245, + "arcLengthMeters": 1.2999999999999989, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.83991089882786107, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.6856564720351217, + "yMeters": 0.90296260218091273, + "headingRadians": 0.072916666666666574, + "unwrappedHeadingRadians": 0.072916666666666574, + "arcLengthMeters": 1.3249999999999988, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.83991089882786107, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.6607138681448349, + "yMeters": 0.90127119454544091, + "headingRadians": 0.06249999999999991, + "unwrappedHeadingRadians": 0.06249999999999991, + "arcLengthMeters": 1.3499999999999988, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.8889074634096118, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.6357549989542689, + "yMeters": 0.89983969276573517, + "headingRadians": 0.052083333333333245, + "unwrappedHeadingRadians": 0.052083333333333245, + "arcLengthMeters": 1.3749999999999987, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.8889074634096118, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.61078257264956826, + "yMeters": 0.89866825216827528, + "headingRadians": 0.041666666666666581, + "unwrappedHeadingRadians": 0.041666666666666581, + "arcLengthMeters": 1.3999999999999986, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.94822646753884665, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.585799298887905, + "yMeters": 0.89775699986135138, + "headingRadians": 0.031249999999999917, + "unwrappedHeadingRadians": 0.031249999999999917, + "arcLengthMeters": 1.4249999999999985, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.94822646753884665, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.56080788850346519, + "yMeters": 0.89710603472127337, + "headingRadians": 0.020833333333333252, + "unwrappedHeadingRadians": 0.020833333333333252, + "arcLengthMeters": 1.4499999999999984, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.99703554474769607, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.5358110532133068, + "yMeters": 0.89671542738164045, + "headingRadians": 0.010416666666666586, + "unwrappedHeadingRadians": 0.010416666666666586, + "arcLengthMeters": 1.4749999999999983, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.99703554474769607, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.51081150532312258, + "yMeters": 0.89658522022567866, + "headingRadians": -1.9081958235744878E-17, + "unwrappedHeadingRadians": -1.9081958235744878E-17, + "arcLengthMeters": 1.4999999999999982, + "direction": 1, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 1.0459143886750764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.51081150532312258, + "yMeters": 0.89658522022567866, + "headingRadians": -1.9081958235744878E-17, + "unwrappedHeadingRadians": -1.9081958235744878E-17, + "arcLengthMeters": 1.4999999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 1.0459143886750764, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 0.53580969691329394, + "yMeters": 0.896845627473476, + "headingRadians": 0.020833333333333315, + "unwrappedHeadingRadians": 0.020833333333333315, + "arcLengthMeters": 1.5249999999999981, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 1.0459143886750764, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.56079703898634548, + "yMeters": 0.89762673619697686, + "headingRadians": 0.04166666666666665, + "unwrappedHeadingRadians": 0.04166666666666665, + "arcLengthMeters": 1.549999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.99703554474769607, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.5857626867339788, + "yMeters": 0.89892820738555967, + "headingRadians": 0.062499999999999986, + "unwrappedHeadingRadians": 0.062499999999999986, + "arcLengthMeters": 1.574999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.99703554474769607, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.61069580476349483, + "yMeters": 0.90074947618500723, + "headingRadians": 0.083333333333333315, + "unwrappedHeadingRadians": 0.083333333333333315, + "arcLengthMeters": 1.5999999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.93796694971647054, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.63558557180048514, + "yMeters": 0.90308975214266041, + "headingRadians": 0.10416666666666666, + "unwrappedHeadingRadians": 0.10416666666666666, + "arcLengthMeters": 1.6249999999999978, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.93796694971647054, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.66042118538539574, + "yMeters": 0.90594801955048387, + "headingRadians": 0.125, + "unwrappedHeadingRadians": 0.125, + "arcLengthMeters": 1.6499999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.8889074634096118, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.68519186656192488, + "yMeters": 0.90932303788589541, + "headingRadians": 0.14583333333333334, + "unwrappedHeadingRadians": 0.14583333333333334, + "arcLengthMeters": 1.6749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.8889074634096118, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.70988686455522054, + "yMeters": 0.9132133423501686, + "headingRadians": 0.16666666666666669, + "unwrappedHeadingRadians": 0.16666666666666669, + "arcLengthMeters": 1.6999999999999975, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.83991089882786107, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.73449546143784639, + "yMeters": 0.91761724450417226, + "headingRadians": 0.18750000000000003, + "unwrappedHeadingRadians": 0.18750000000000003, + "arcLengthMeters": 1.7249999999999974, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.83991089882786107, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.75900697678149176, + "yMeters": 0.92253283300117417, + "headingRadians": 0.20833333333333337, + "unwrappedHeadingRadians": 0.20833333333333337, + "arcLengthMeters": 1.7499999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.79098371565941161, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.78341077229240685, + "yMeters": 0.92795797441638772, + "headingRadians": 0.22916666666666671, + "unwrappedHeadingRadians": 0.22916666666666671, + "arcLengthMeters": 1.7749999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.79098371565941161, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.80769625642855014, + "yMeters": 0.933890314172905, + "headingRadians": 0.25000000000000006, + "unwrappedHeadingRadians": 0.25000000000000006, + "arcLengthMeters": 1.7999999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.74213327792570938, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.831852888996446, + "yMeters": 0.94032727756361079, + "headingRadians": 0.27083333333333337, + "unwrappedHeadingRadians": 0.27083333333333337, + "arcLengthMeters": 1.8249999999999971, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.74213327792570938, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.85587018572575635, + "yMeters": 0.94726607086863746, + "headingRadians": 0.29166666666666669, + "unwrappedHeadingRadians": 0.29166666666666669, + "arcLengthMeters": 1.849999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.69336801574440965, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.87973772281957963, + "yMeters": 0.954703682567872, + "headingRadians": 0.3125, + "unwrappedHeadingRadians": 0.3125, + "arcLengthMeters": 1.8749999999999969, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.6830218388191488, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.9034451414785053, + "yMeters": 0.96263688464799346, + "headingRadians": 0.33333333333333331, + "unwrappedHeadingRadians": 0.33333333333333331, + "arcLengthMeters": 1.8999999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.63395648772997248, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.92698215239645787, + "yMeters": 0.97106223400346858, + "headingRadians": 0.35416666666666663, + "unwrappedHeadingRadians": 0.35416666666666663, + "arcLengthMeters": 1.9249999999999967, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.63395648772997248, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.95033854022637976, + "yMeters": 0.97997607393090158, + "headingRadians": 0.37499999999999994, + "unwrappedHeadingRadians": 0.37499999999999994, + "arcLengthMeters": 1.9499999999999966, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.58496667654490253, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.97350416801381545, + "yMeters": 0.9893745357160878, + "headingRadians": 0.39583333333333326, + "unwrappedHeadingRadians": 0.39583333333333326, + "arcLengthMeters": 1.9749999999999965, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.58496667654490253, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 0.99646898159647213, + "yMeters": 0.99925354031308233, + "headingRadians": 0.41666666666666646, + "unwrappedHeadingRadians": 0.41666666666666646, + "arcLengthMeters": 1.9999999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.58496667654490253, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.019223013967848, + "yMeters": 1.0096088001145549, + "headingRadians": 0.43749999999999978, + "unwrappedHeadingRadians": 0.43749999999999978, + "arcLengthMeters": 2.0249999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0417563896030329, + "yMeters": 1.0204358208126638, + "headingRadians": 0.45833333333333309, + "unwrappedHeadingRadians": 0.45833333333333309, + "arcLengthMeters": 2.0499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.52646336232465141, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0640593287448055, + "yMeters": 1.0317299033496392, + "headingRadians": 0.47916666666666641, + "unwrappedHeadingRadians": 0.47916666666666641, + "arcLengthMeters": 2.0749999999999962, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.47723559808167104, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0861221516481661, + "yMeters": 1.0434861459572313, + "headingRadians": 0.49999999999999972, + "unwrappedHeadingRadians": 0.49999999999999972, + "arcLengthMeters": 2.0999999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.47723559808167104, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1079352827814632, + "yMeters": 1.0556994462841371, + "headingRadians": 0.520833333333333, + "unwrappedHeadingRadians": 0.520833333333333, + "arcLengthMeters": 2.124999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.41986178111159767, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1294892549822901, + "yMeters": 1.0683645036104821, + "headingRadians": 0.54166666666666641, + "unwrappedHeadingRadians": 0.54166666666666641, + "arcLengthMeters": 2.1499999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.41986178111159767, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1507747135663466, + "yMeters": 1.081475821148397, + "headingRadians": 0.56249999999999978, + "unwrappedHeadingRadians": 0.56249999999999978, + "arcLengthMeters": 2.1749999999999958, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.37038903013961044, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.171782420387486, + "yMeters": 1.0950277084276903, + "headingRadians": 0.58333333333333315, + "unwrappedHeadingRadians": 0.58333333333333315, + "arcLengthMeters": 2.1999999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.37038903013961044, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1925032578471824, + "yMeters": 1.1090142837655803, + "headingRadians": 0.60416666666666652, + "unwrappedHeadingRadians": 0.60416666666666652, + "arcLengthMeters": 2.2249999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.3641891714438652, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2129282328516771, + "yMeters": 1.1234294768194173, + "headingRadians": 0.62499999999999989, + "unwrappedHeadingRadians": 0.62499999999999989, + "arcLengthMeters": 2.2499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.31445026805891629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2330484807150914, + "yMeters": 1.1382670312212853, + "headingRadians": 0.64583333333333326, + "unwrappedHeadingRadians": 0.64583333333333326, + "arcLengthMeters": 2.2749999999999955, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.31445026805891629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2528552690068069, + "yMeters": 1.1535205072933412, + "headingRadians": 0.66666666666666663, + "unwrappedHeadingRadians": 0.66666666666666663, + "arcLengthMeters": 2.2999999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.26058942819777364, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2723400013414456, + "yMeters": 1.1691832848427131, + "headingRadians": 0.6875, + "unwrappedHeadingRadians": 0.6875, + "arcLengthMeters": 2.3249999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.26058942819777364, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2914942211098055, + "yMeters": 1.1852485660347432, + "headingRadians": 0.70833333333333337, + "unwrappedHeadingRadians": 0.70833333333333337, + "arcLengthMeters": 2.3499999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.26058942819777364, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3103096151491305, + "yMeters": 1.201709378343331, + "headingRadians": 0.72916666666666674, + "unwrappedHeadingRadians": 0.72916666666666674, + "arcLengthMeters": 2.3749999999999951, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3287780173511237, + "yMeters": 1.218558577577094, + "headingRadians": 0.75000000000000011, + "unwrappedHeadingRadians": 0.75000000000000011, + "arcLengthMeters": 2.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3468914122061377, + "yMeters": 1.2357888509800314, + "headingRadians": 0.77083333333333348, + "unwrappedHeadingRadians": 0.77083333333333348, + "arcLengthMeters": 2.4249999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3646419382820014, + "yMeters": 1.253392720405351, + "headingRadians": 0.79166666666666685, + "unwrappedHeadingRadians": 0.79166666666666685, + "arcLengthMeters": 2.4499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3820218916359779, + "yMeters": 1.2713625455610733, + "headingRadians": 0.81250000000000022, + "unwrappedHeadingRadians": 0.81250000000000022, + "arcLengthMeters": 2.4749999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3990237291583674, + "yMeters": 1.2896905273260111, + "headingRadians": 0.83333333333333348, + "unwrappedHeadingRadians": 0.83333333333333348, + "arcLengthMeters": 2.4999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.83333333333333337, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4157373549333043, + "yMeters": 1.3082821669006273, + "headingRadians": 0.84375000000000011, + "unwrappedHeadingRadians": 0.84375000000000011, + "arcLengthMeters": 2.5249999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4322564145342496, + "yMeters": 1.3270468949433543, + "headingRadians": 0.85416666666666674, + "unwrappedHeadingRadians": 0.85416666666666674, + "arcLengthMeters": 2.5499999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4485791155447281, + "yMeters": 1.3459826753692992, + "headingRadians": 0.86458333333333337, + "unwrappedHeadingRadians": 0.86458333333333337, + "arcLengthMeters": 2.5749999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.464703686854343, + "yMeters": 1.3650874535333666, + "headingRadians": 0.875, + "unwrappedHeadingRadians": 0.875, + "arcLengthMeters": 2.5999999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4806283788509516, + "yMeters": 1.3843591564531978, + "headingRadians": 0.88541666666666663, + "unwrappedHeadingRadians": 0.88541666666666663, + "arcLengthMeters": 2.6249999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4963514636105082, + "yMeters": 1.4037956930341031, + "headingRadians": 0.89583333333333326, + "unwrappedHeadingRadians": 0.89583333333333326, + "arcLengthMeters": 2.6499999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5118712350845553, + "yMeters": 1.4233949542959579, + "headingRadians": 0.90624999999999989, + "unwrappedHeadingRadians": 0.90624999999999989, + "arcLengthMeters": 2.674999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5271860092853389, + "yMeters": 1.4431548136020385, + "headingRadians": 0.91666666666666652, + "unwrappedHeadingRadians": 0.91666666666666652, + "arcLengthMeters": 2.699999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5422941244685313, + "yMeters": 1.4630731268897763, + "headingRadians": 0.92708333333333315, + "unwrappedHeadingRadians": 0.92708333333333315, + "arcLengthMeters": 2.7249999999999939, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5571939413135409, + "yMeters": 1.4831477329034006, + "headingRadians": 0.93749999999999978, + "unwrappedHeadingRadians": 0.93749999999999978, + "arcLengthMeters": 2.7499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0092016093317762726, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5718838431013882, + "yMeters": 1.5033764534284484, + "headingRadians": 0.94791666666666641, + "unwrappedHeadingRadians": 0.94791666666666641, + "arcLengthMeters": 2.7749999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0092016093317762726, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5863622358901286, + "yMeters": 1.5237570935281122, + "headingRadians": 0.958333333333333, + "unwrappedHeadingRadians": 0.958333333333333, + "arcLengthMeters": 2.7999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6006275486878057, + "yMeters": 1.5442874417814059, + "headingRadians": 0.96874999999999967, + "unwrappedHeadingRadians": 0.96874999999999967, + "arcLengthMeters": 2.8249999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6146782336229126, + "yMeters": 1.5649652705231147, + "headingRadians": 0.9791666666666663, + "unwrappedHeadingRadians": 0.9791666666666663, + "arcLengthMeters": 2.8499999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6285127661123453, + "yMeters": 1.5857883360855125, + "headingRadians": 0.98958333333333293, + "unwrappedHeadingRadians": 0.98958333333333293, + "arcLengthMeters": 2.8749999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.642129645026829, + "yMeters": 1.6067543790418111, + "headingRadians": 0.99999999999999956, + "unwrappedHeadingRadians": 0.99999999999999956, + "arcLengthMeters": 2.8999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6555273928538004, + "yMeters": 1.6278611244513235, + "headingRadians": 1.0104166666666663, + "unwrappedHeadingRadians": 1.0104166666666663, + "arcLengthMeters": 2.9249999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6687045558577251, + "yMeters": 1.6491062821063067, + "headingRadians": 1.020833333333333, + "unwrappedHeadingRadians": 1.020833333333333, + "arcLengthMeters": 2.949999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6816597042378378, + "yMeters": 1.6704875467804639, + "headingRadians": 1.0312499999999998, + "unwrappedHeadingRadians": 1.0312499999999998, + "arcLengthMeters": 2.9749999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6943914322832838, + "yMeters": 1.6920025984790739, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 2.9999999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7070109876441826, + "yMeters": 1.7135837667165259, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.0249999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7196305430050813, + "yMeters": 1.7351649349539779, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.0499999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.73225009836598, + "yMeters": 1.75674610319143, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.0749999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7448696537268789, + "yMeters": 1.778327271428882, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.0999999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7574892090877776, + "yMeters": 1.7999084396663341, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.1249999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7701087644486764, + "yMeters": 1.8214896079037861, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.1499999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7827283198095751, + "yMeters": 1.8430707761412382, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.1749999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7953478751704739, + "yMeters": 1.8646519443786902, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.1999999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8079674305313727, + "yMeters": 1.8862331126161422, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.224999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8205869858922714, + "yMeters": 1.9078142808535943, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.2499999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8332065412531702, + "yMeters": 1.9293954490910463, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.2749999999999928, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8458260966140689, + "yMeters": 1.9509766173284984, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.2999999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8584456519749677, + "yMeters": 1.9725577855659504, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.3249999999999926, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8710652073358665, + "yMeters": 1.9941389538034024, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.3499999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8836847626967652, + "yMeters": 2.0157201220408547, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.3749999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.896304318057664, + "yMeters": 2.037301290278307, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.3999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9089238734185627, + "yMeters": 2.0588824585157592, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.4249999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.011121849852041232, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9215434287794615, + "yMeters": 2.0804636267532115, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.449999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.030022002581713281, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9341629841403603, + "yMeters": 2.1020447949906638, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.4749999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.030022002581713281, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9467825395012588, + "yMeters": 2.1236259632281156, + "headingRadians": 1.0416666666666663, + "unwrappedHeadingRadians": 1.0416666666666663, + "arcLengthMeters": 3.4999999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.075092662495593987, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.959514267546705, + "yMeters": 2.1451410149267263, + "headingRadians": 1.0312499999999996, + "unwrappedHeadingRadians": 1.0312499999999996, + "arcLengthMeters": 3.5249999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.053823808804266515, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9724694159268177, + "yMeters": 2.1665222796008834, + "headingRadians": 1.0208333333333328, + "unwrappedHeadingRadians": 1.0208333333333328, + "arcLengthMeters": 3.5499999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.053823808804266515, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9856465789307425, + "yMeters": 2.1877674372558666, + "headingRadians": 1.0104166666666661, + "unwrappedHeadingRadians": 1.0104166666666661, + "arcLengthMeters": 3.5749999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9990443267577138, + "yMeters": 2.208874182665379, + "headingRadians": 0.99999999999999944, + "unwrappedHeadingRadians": 0.99999999999999944, + "arcLengthMeters": 3.5999999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.35920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0126612056721975, + "yMeters": 2.2298402256216776, + "headingRadians": 0.98958333333333282, + "unwrappedHeadingRadians": 0.98958333333333282, + "arcLengthMeters": 3.6249999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.02649573816163, + "yMeters": 2.2506632911840754, + "headingRadians": 0.97916666666666619, + "unwrappedHeadingRadians": 0.97916666666666619, + "arcLengthMeters": 3.6499999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0405464230967372, + "yMeters": 2.271341119925784, + "headingRadians": 0.96874999999999956, + "unwrappedHeadingRadians": 0.96874999999999956, + "arcLengthMeters": 3.6749999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.30920160933177632, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0548117358944142, + "yMeters": 2.2918714681790773, + "headingRadians": 0.95833333333333293, + "unwrappedHeadingRadians": 0.95833333333333293, + "arcLengthMeters": 3.6999999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0692901286831549, + "yMeters": 2.3122521082787411, + "headingRadians": 0.9479166666666663, + "unwrappedHeadingRadians": 0.9479166666666663, + "arcLengthMeters": 3.7249999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0839800304710021, + "yMeters": 2.3324808288037886, + "headingRadians": 0.93749999999999967, + "unwrappedHeadingRadians": 0.93749999999999967, + "arcLengthMeters": 3.7499999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0988798473160117, + "yMeters": 2.3525554348174129, + "headingRadians": 0.927083333333333, + "unwrappedHeadingRadians": 0.927083333333333, + "arcLengthMeters": 3.7749999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.25920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1139879624992042, + "yMeters": 2.3724737481051505, + "headingRadians": 0.91666666666666641, + "unwrappedHeadingRadians": 0.91666666666666641, + "arcLengthMeters": 3.799999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1293027366999877, + "yMeters": 2.3922336074112311, + "headingRadians": 0.90624999999999978, + "unwrappedHeadingRadians": 0.90624999999999978, + "arcLengthMeters": 3.824999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1448225081740349, + "yMeters": 2.4118328686730859, + "headingRadians": 0.89583333333333315, + "unwrappedHeadingRadians": 0.89583333333333315, + "arcLengthMeters": 3.8499999999999939, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1605455929335919, + "yMeters": 2.4312694052539912, + "headingRadians": 0.88541666666666652, + "unwrappedHeadingRadians": 0.88541666666666652, + "arcLengthMeters": 3.8749999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1764702849302004, + "yMeters": 2.4505411081738222, + "headingRadians": 0.87499999999999989, + "unwrappedHeadingRadians": 0.87499999999999989, + "arcLengthMeters": 3.8999999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1925948562398152, + "yMeters": 2.4696458863378896, + "headingRadians": 0.86458333333333326, + "unwrappedHeadingRadians": 0.86458333333333326, + "arcLengthMeters": 3.9249999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.15920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2089175572502935, + "yMeters": 2.4885816667638347, + "headingRadians": 0.85416666666666663, + "unwrappedHeadingRadians": 0.85416666666666663, + "arcLengthMeters": 3.9499999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2254366168512387, + "yMeters": 2.5073463948065617, + "headingRadians": 0.84375, + "unwrappedHeadingRadians": 0.84375, + "arcLengthMeters": 3.9749999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2421502426261757, + "yMeters": 2.5259380343811779, + "headingRadians": 0.83333333333333337, + "unwrappedHeadingRadians": 0.83333333333333337, + "arcLengthMeters": 3.9999999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2590566210470397, + "yMeters": 2.5443545681839219, + "headingRadians": 0.82291666666666674, + "unwrappedHeadingRadians": 0.82291666666666674, + "arcLengthMeters": 4.0249999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2761539176709542, + "yMeters": 2.5625939979110535, + "headingRadians": 0.81250000000000011, + "unwrappedHeadingRadians": 0.81250000000000011, + "arcLengthMeters": 4.0499999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2934402773392795, + "yMeters": 2.5806543444756791, + "headingRadians": 0.80208333333333348, + "unwrappedHeadingRadians": 0.80208333333333348, + "arcLengthMeters": 4.074999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.059201609331776317, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3109138243789071, + "yMeters": 2.5985336482224977, + "headingRadians": 0.79166666666666685, + "unwrappedHeadingRadians": 0.79166666666666685, + "arcLengthMeters": 4.0999999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0092016093317762726, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3285726628057835, + "yMeters": 2.6162299691404329, + "headingRadians": 0.78125000000000022, + "unwrappedHeadingRadians": 0.78125000000000022, + "arcLengthMeters": 4.1249999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0092016093317762726, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3464148765306345, + "yMeters": 2.6337413870731368, + "headingRadians": 0.77083333333333359, + "unwrappedHeadingRadians": 0.77083333333333359, + "arcLengthMeters": 4.149999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0092016093317762726, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3644385295668724, + "yMeters": 2.6510660019273375, + "headingRadians": 0.760416666666667, + "unwrappedHeadingRadians": 0.760416666666667, + "arcLengthMeters": 4.1749999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3826416662406622, + "yMeters": 2.6682019338790117, + "headingRadians": 0.75000000000000033, + "unwrappedHeadingRadians": 0.75000000000000033, + "arcLengthMeters": 4.1999999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4010223114031244, + "yMeters": 2.6851473235773557, + "headingRadians": 0.7395833333333337, + "unwrappedHeadingRadians": 0.7395833333333337, + "arcLengthMeters": 4.2249999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4195784706446486, + "yMeters": 2.7019003323465371, + "headingRadians": 0.72916666666666707, + "unwrappedHeadingRadians": 0.72916666666666707, + "arcLengthMeters": 4.2499999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4383081305113019, + "yMeters": 2.7184591423852007, + "headingRadians": 0.71875000000000044, + "unwrappedHeadingRadians": 0.71875000000000044, + "arcLengthMeters": 4.2749999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.50920160933177627, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4572092587232981, + "yMeters": 2.7348219569637124, + "headingRadians": 0.70833333333333381, + "unwrappedHeadingRadians": 0.70833333333333381, + "arcLengthMeters": 4.2999999999999972, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4762798043955128, + "yMeters": 2.7509870006191144, + "headingRadians": 0.69791666666666718, + "unwrappedHeadingRadians": 0.69791666666666718, + "arcLengthMeters": 4.3249999999999975, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4955176982600173, + "yMeters": 2.7669525193477726, + "headingRadians": 0.68750000000000056, + "unwrappedHeadingRadians": 0.68750000000000056, + "arcLengthMeters": 4.3499999999999979, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.45920160933177623, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5149208528906062, + "yMeters": 2.7827167807956981, + "headingRadians": 0.67708333333333393, + "unwrappedHeadingRadians": 0.67708333333333393, + "arcLengthMeters": 4.3749999999999982, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5344871629292949, + "yMeters": 2.7982780744465163, + "headingRadians": 0.6666666666666673, + "unwrappedHeadingRadians": 0.6666666666666673, + "arcLengthMeters": 4.3999999999999986, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.40920160933177629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5542145053147642, + "yMeters": 2.8136347118070697, + "headingRadians": 0.65625000000000067, + "unwrappedHeadingRadians": 0.65625000000000067, + "arcLengthMeters": 4.4249999999999989, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.36045082905681558, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5741007395127258, + "yMeters": 2.8287850265906282, + "headingRadians": 0.645833333333334, + "unwrappedHeadingRadians": 0.645833333333334, + "arcLengthMeters": 4.4499999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.36045082905681558, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5941437077481808, + "yMeters": 2.8437273748976919, + "headingRadians": 0.63541666666666741, + "unwrappedHeadingRadians": 0.63541666666666741, + "arcLengthMeters": 4.475, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.36045082905681558, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6143412352395541, + "yMeters": 2.8584601353943642, + "headingRadians": 0.62500000000000078, + "unwrappedHeadingRadians": 0.62500000000000078, + "arcLengthMeters": 4.5, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.31445026805891629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6346911304346685, + "yMeters": 2.8729817094882746, + "headingRadians": 0.61458333333333415, + "unwrappedHeadingRadians": 0.61458333333333415, + "arcLengthMeters": 4.525, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.31445026805891629, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.655191185248543, + "yMeters": 2.8872905215020381, + "headingRadians": 0.60416666666666752, + "unwrappedHeadingRadians": 0.60416666666666752, + "arcLengthMeters": 4.5500000000000007, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.264740123145518, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6758391753029822, + "yMeters": 2.901385018844223, + "headingRadians": 0.59375000000000089, + "unwrappedHeadingRadians": 0.59375000000000089, + "arcLengthMeters": 4.5750000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.264740123145518, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6966328601679352, + "yMeters": 2.9152636721778178, + "headingRadians": 0.58333333333333426, + "unwrappedHeadingRadians": 0.58333333333333426, + "arcLengthMeters": 4.6000000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.27161598887650928, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7175699836045948, + "yMeters": 2.9289249755861735, + "headingRadians": 0.57291666666666763, + "unwrappedHeadingRadians": 0.57291666666666763, + "arcLengthMeters": 4.6250000000000018, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.22233543441337966, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.738648273810214, + "yMeters": 2.9423674467364038, + "headingRadians": 0.562500000000001, + "unwrappedHeadingRadians": 0.562500000000001, + "arcLengthMeters": 4.6500000000000021, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.22233543441337966, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7598654436646091, + "yMeters": 2.9555896270402275, + "headingRadians": 0.55208333333333437, + "unwrappedHeadingRadians": 0.55208333333333437, + "arcLengthMeters": 4.6750000000000025, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7812191909783266, + "yMeters": 2.9685900818122337, + "headingRadians": 0.54166666666666774, + "unwrappedHeadingRadians": 0.54166666666666774, + "arcLengthMeters": 4.7000000000000028, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.18382273445530839, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8027071987424441, + "yMeters": 2.9813674004255537, + "headingRadians": 0.53125000000000111, + "unwrappedHeadingRadians": 0.53125000000000111, + "arcLengthMeters": 4.7250000000000032, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.82432713537998, + "yMeters": 2.9939201964649236, + "headingRadians": 0.52083333333333448, + "unwrappedHeadingRadians": 0.52083333333333448, + "arcLengthMeters": 4.7500000000000036, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8460766549988827, + "yMeters": 3.0062471078771162, + "headingRadians": 0.51041666666666785, + "unwrappedHeadingRadians": 0.51041666666666785, + "arcLengthMeters": 4.7750000000000039, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8679533976465748, + "yMeters": 3.0183467971187348, + "headingRadians": 0.50000000000000122, + "unwrappedHeadingRadians": 0.50000000000000122, + "arcLengthMeters": 4.8000000000000043, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.889954989566021, + "yMeters": 3.0302179513013425, + "headingRadians": 0.48958333333333454, + "unwrappedHeadingRadians": 0.48958333333333454, + "arcLengthMeters": 4.8250000000000046, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.10250504669770155, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9120790434532959, + "yMeters": 3.0418592823339194, + "headingRadians": 0.47916666666666785, + "unwrappedHeadingRadians": 0.47916666666666785, + "arcLengthMeters": 4.850000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.055621023190982277, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9343231587166207, + "yMeters": 3.0532695270626271, + "headingRadians": 0.46875000000000117, + "unwrappedHeadingRadians": 0.46875000000000117, + "arcLengthMeters": 4.8750000000000053, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.055621023190982277, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9566849217368412, + "yMeters": 3.0644474474078702, + "headingRadians": 0.45833333333333448, + "unwrappedHeadingRadians": 0.45833333333333448, + "arcLengthMeters": 4.9000000000000057, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.030022002581713281, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9791619061293191, + "yMeters": 3.0753918304986341, + "headingRadians": 0.4479166666666678, + "unwrappedHeadingRadians": 0.4479166666666678, + "arcLengthMeters": 4.925000000000006, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.030022002581713281, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0017516730072109, + "yMeters": 3.0861014888040881, + "headingRadians": 0.43750000000000111, + "unwrappedHeadingRadians": 0.43750000000000111, + "arcLengthMeters": 4.9500000000000064, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0244517712461012, + "yMeters": 3.0965752602624419, + "headingRadians": 0.42708333333333443, + "unwrappedHeadingRadians": 0.42708333333333443, + "arcLengthMeters": 4.9750000000000068, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0472597377499628, + "yMeters": 3.1068120084070334, + "headingRadians": 0.4166666666666678, + "unwrappedHeadingRadians": 0.4166666666666678, + "arcLengthMeters": 5.0000000000000071, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0702245513326196, + "yMeters": 3.116691013004028, + "headingRadians": 0.39583333333333448, + "unwrappedHeadingRadians": 0.39583333333333448, + "arcLengthMeters": 5.0250000000000075, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0933901791200555, + "yMeters": 3.1260894747892141, + "headingRadians": 0.37500000000000117, + "unwrappedHeadingRadians": 0.37500000000000117, + "arcLengthMeters": 5.0500000000000078, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1167465669499772, + "yMeters": 3.1350033147166472, + "headingRadians": 0.35416666666666785, + "unwrappedHeadingRadians": 0.35416666666666785, + "arcLengthMeters": 5.0750000000000082, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1402835778679297, + "yMeters": 3.1434286640721223, + "headingRadians": 0.33333333333333454, + "unwrappedHeadingRadians": 0.33333333333333454, + "arcLengthMeters": 5.1000000000000085, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1639909965268553, + "yMeters": 3.1513618661522438, + "headingRadians": 0.31250000000000122, + "unwrappedHeadingRadians": 0.31250000000000122, + "arcLengthMeters": 5.1250000000000089, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1878585336206786, + "yMeters": 3.1587994778514785, + "headingRadians": 0.29166666666666791, + "unwrappedHeadingRadians": 0.29166666666666791, + "arcLengthMeters": 5.1500000000000092, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2118758303499888, + "yMeters": 3.165738271156505, + "headingRadians": 0.27083333333333459, + "unwrappedHeadingRadians": 0.27083333333333459, + "arcLengthMeters": 5.17500000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2360324629178847, + "yMeters": 3.1721752345472107, + "headingRadians": 0.25000000000000128, + "unwrappedHeadingRadians": 0.25000000000000128, + "arcLengthMeters": 5.20000000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2603179470540278, + "yMeters": 3.1781075743037279, + "headingRadians": 0.22916666666666793, + "unwrappedHeadingRadians": 0.22916666666666793, + "arcLengthMeters": 5.22500000000001, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 0.0, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2847217425649426, + "yMeters": 3.1835327157189415, + "headingRadians": 0.20833333333333459, + "unwrappedHeadingRadians": 0.20833333333333459, + "arcLengthMeters": 5.2500000000000107, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3092332579085881, + "yMeters": 3.1884483042159433, + "headingRadians": 0.18750000000000125, + "unwrappedHeadingRadians": 0.18750000000000125, + "arcLengthMeters": 5.275000000000011, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3338418547912139, + "yMeters": 3.1928522063699472, + "headingRadians": 0.16666666666666791, + "unwrappedHeadingRadians": 0.16666666666666791, + "arcLengthMeters": 5.3000000000000114, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3585368527845096, + "yMeters": 3.1967425108342207, + "headingRadians": 0.14583333333333456, + "unwrappedHeadingRadians": 0.14583333333333456, + "arcLengthMeters": 5.3250000000000117, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3833075339610388, + "yMeters": 3.2001175291696322, + "headingRadians": 0.12500000000000122, + "unwrappedHeadingRadians": 0.12500000000000122, + "arcLengthMeters": 5.3500000000000121, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4081431475459492, + "yMeters": 3.2029757965774555, + "headingRadians": 0.10416666666666788, + "unwrappedHeadingRadians": 0.10416666666666788, + "arcLengthMeters": 5.3750000000000124, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4330329145829395, + "yMeters": 3.2053160725351084, + "headingRadians": 0.083333333333334536, + "unwrappedHeadingRadians": 0.083333333333334536, + "arcLengthMeters": 5.4000000000000128, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4579660326124557, + "yMeters": 3.2071373413345561, + "headingRadians": 0.0625000000000012, + "unwrappedHeadingRadians": 0.0625000000000012, + "arcLengthMeters": 5.4250000000000131, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4829316803600889, + "yMeters": 3.2084388125231387, + "headingRadians": 0.041666666666667858, + "unwrappedHeadingRadians": 0.041666666666667858, + "arcLengthMeters": 5.4500000000000135, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5079190224331405, + "yMeters": 3.2092199212466395, + "headingRadians": 0.020833333333334522, + "unwrappedHeadingRadians": 0.020833333333334522, + "arcLengthMeters": 5.4750000000000139, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5329172140233118, + "yMeters": 3.2094803284944367, + "headingRadians": 1.3079815008865125E-15, + "unwrappedHeadingRadians": 1.3079815008865125E-15, + "arcLengthMeters": 5.5000000000000133, + "direction": 0, + "vehicleCurvaturePerMeter": -0.83333333333333337, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5579167619134964, + "yMeters": 3.209350121338475, + "headingRadians": -0.01041666666666536, + "unwrappedHeadingRadians": -0.01041666666666536, + "arcLengthMeters": 5.5250000000000137, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.582913597203655, + "yMeters": 3.2089595139988423, + "headingRadians": -0.020833333333332028, + "unwrappedHeadingRadians": -0.020833333333332028, + "arcLengthMeters": 5.550000000000014, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6079050075880947, + "yMeters": 3.2083085488587644, + "headingRadians": -0.031249999999998695, + "unwrappedHeadingRadians": -0.031249999999998695, + "arcLengthMeters": 5.5750000000000144, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6328882813497581, + "yMeters": 3.2073972965518407, + "headingRadians": -0.041666666666665367, + "unwrappedHeadingRadians": -0.041666666666665367, + "arcLengthMeters": 5.6000000000000147, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6578607076544589, + "yMeters": 3.2062258559543806, + "headingRadians": -0.052083333333332038, + "unwrappedHeadingRadians": -0.052083333333332038, + "arcLengthMeters": 5.6250000000000151, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6828195768450249, + "yMeters": 3.2047943541746751, + "headingRadians": -0.062499999999998709, + "unwrappedHeadingRadians": -0.062499999999998709, + "arcLengthMeters": 5.6500000000000155, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7077621807353118, + "yMeters": 3.2031029465392034, + "headingRadians": -0.072916666666665381, + "unwrappedHeadingRadians": -0.072916666666665381, + "arcLengthMeters": 5.6750000000000158, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.732685812904057, + "yMeters": 3.20115181657578, + "headingRadians": -0.083333333333332052, + "unwrappedHeadingRadians": -0.083333333333332052, + "arcLengthMeters": 5.7000000000000162, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7575877689885435, + "yMeters": 3.1989411759936415, + "headingRadians": -0.093749999999998723, + "unwrappedHeadingRadians": -0.093749999999998723, + "arcLengthMeters": 5.7250000000000165, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.782465346978038, + "yMeters": 3.1964712646604738, + "headingRadians": -0.10416666666666539, + "unwrappedHeadingRadians": -0.10416666666666539, + "arcLengthMeters": 5.7500000000000169, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8073158475069762, + "yMeters": 3.1937423505763851, + "headingRadians": -0.11458333333333207, + "unwrappedHeadingRadians": -0.11458333333333207, + "arcLengthMeters": 5.7750000000000172, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8321365741478592, + "yMeters": 3.1907547298448273, + "headingRadians": -0.12499999999999874, + "unwrappedHeadingRadians": -0.12499999999999874, + "arcLengthMeters": 5.8000000000000176, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8569248337038338, + "yMeters": 3.1875087266404654, + "headingRadians": -0.13541666666666541, + "unwrappedHeadingRadians": -0.13541666666666541, + "arcLengthMeters": 5.8250000000000179, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8816779365009175, + "yMeters": 3.1840046931740043, + "headingRadians": -0.14583333333333207, + "unwrappedHeadingRadians": -0.14583333333333207, + "arcLengthMeters": 5.8500000000000183, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9063931966798471, + "yMeters": 3.1802430096539704, + "headingRadians": -0.15624999999999872, + "unwrappedHeadingRadians": -0.15624999999999872, + "arcLengthMeters": 5.8750000000000187, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9310679324875086, + "yMeters": 3.1762240842454581, + "headingRadians": -0.16666666666666538, + "unwrappedHeadingRadians": -0.16666666666666538, + "arcLengthMeters": 5.900000000000019, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9556994665679239, + "yMeters": 3.1719483530258397, + "headingRadians": -0.17708333333333204, + "unwrappedHeadingRadians": -0.17708333333333204, + "arcLengthMeters": 5.9250000000000194, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.98028512625276, + "yMeters": 3.1674162799374508, + "headingRadians": -0.1874999999999987, + "unwrappedHeadingRadians": -0.1874999999999987, + "arcLengthMeters": 5.95000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0048222438513292, + "yMeters": 3.1626283567372466, + "headingRadians": -0.19791666666666535, + "unwrappedHeadingRadians": -0.19791666666666535, + "arcLengthMeters": 5.97500000000002, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0293081569400506, + "yMeters": 3.1575851029434472, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.0000000000000195, + "direction": 0, + "vehicleCurvaturePerMeter": -0.41666666666666669, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0537675816738945, + "yMeters": 3.1524143639547311, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.02500000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0782270064077384, + "yMeters": 3.147243624966015, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.05000000000002, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1026864311415823, + "yMeters": 3.1420728859772988, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.0750000000000206, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1271458558754262, + "yMeters": 3.1369021469885827, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.100000000000021, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.15160528060927, + "yMeters": 3.1317314079998666, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.1250000000000213, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1760647053431139, + "yMeters": 3.1265606690111505, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.1500000000000217, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2005241300769578, + "yMeters": 3.1213899300224344, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.175000000000022, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2249835548108017, + "yMeters": 3.1162191910337182, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.2000000000000224, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2494429795446456, + "yMeters": 3.1110484520450021, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.2250000000000227, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2739024042784894, + "yMeters": 3.105877713056286, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.2500000000000231, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2983618290123333, + "yMeters": 3.10070697406757, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.2750000000000234, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3228212537461772, + "yMeters": 3.0955362350788538, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.3000000000000238, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3472806784800211, + "yMeters": 3.0903654960901377, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.3250000000000242, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.371740103213865, + "yMeters": 3.0851947571014215, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.3500000000000245, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3961995279477089, + "yMeters": 3.0800240181127054, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.3750000000000249, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4206589526815527, + "yMeters": 3.0748532791239893, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.4000000000000252, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4451183774153966, + "yMeters": 3.0696825401352732, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.4250000000000256, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4695778021492405, + "yMeters": 3.0645118011465571, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.4500000000000259, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4940372268830844, + "yMeters": 3.059341062157841, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.4750000000000263, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5184966516169283, + "yMeters": 3.0541703231691248, + "headingRadians": -0.20833333333333195, + "unwrappedHeadingRadians": -0.20833333333333195, + "arcLengthMeters": 6.5000000000000266, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.54298256470565, + "yMeters": 3.0491270693753254, + "headingRadians": -0.1979166666666653, + "unwrappedHeadingRadians": -0.1979166666666653, + "arcLengthMeters": 6.525000000000027, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5675196823042192, + "yMeters": 3.0443391461751212, + "headingRadians": -0.18749999999999864, + "unwrappedHeadingRadians": -0.18749999999999864, + "arcLengthMeters": 6.5500000000000274, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5921053419890558, + "yMeters": 3.0398070730867324, + "headingRadians": -0.17708333333333198, + "unwrappedHeadingRadians": -0.17708333333333198, + "arcLengthMeters": 6.5750000000000277, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6167368760694716, + "yMeters": 3.0355313418671139, + "headingRadians": -0.16666666666666533, + "unwrappedHeadingRadians": -0.16666666666666533, + "arcLengthMeters": 6.6000000000000281, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6414116118771336, + "yMeters": 3.0315124164586016, + "headingRadians": -0.15624999999999867, + "unwrappedHeadingRadians": -0.15624999999999867, + "arcLengthMeters": 6.6250000000000284, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6661268720560631, + "yMeters": 3.0277507329385678, + "headingRadians": -0.145833333333332, + "unwrappedHeadingRadians": -0.145833333333332, + "arcLengthMeters": 6.6500000000000288, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6908799748531473, + "yMeters": 3.0242466994721062, + "headingRadians": -0.13541666666666535, + "unwrappedHeadingRadians": -0.13541666666666535, + "arcLengthMeters": 6.6750000000000291, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7156682344091214, + "yMeters": 3.0210006962677443, + "headingRadians": -0.12499999999999868, + "unwrappedHeadingRadians": -0.12499999999999868, + "arcLengthMeters": 6.7000000000000295, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7404889610500049, + "yMeters": 3.0180130755361865, + "headingRadians": -0.11458333333333201, + "unwrappedHeadingRadians": -0.11458333333333201, + "arcLengthMeters": 6.72500000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7653394615789431, + "yMeters": 3.0152841614520978, + "headingRadians": -0.10416666666666534, + "unwrappedHeadingRadians": -0.10416666666666534, + "arcLengthMeters": 6.75000000000003, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7902170395684376, + "yMeters": 3.01281425011893, + "headingRadians": -0.093749999999998668, + "unwrappedHeadingRadians": -0.093749999999998668, + "arcLengthMeters": 6.7750000000000306, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8151189956529237, + "yMeters": 3.0106036095367914, + "headingRadians": -0.083333333333332, + "unwrappedHeadingRadians": -0.083333333333332, + "arcLengthMeters": 6.8000000000000309, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8400426278216688, + "yMeters": 3.0086524795733682, + "headingRadians": -0.072916666666665325, + "unwrappedHeadingRadians": -0.072916666666665325, + "arcLengthMeters": 6.8250000000000313, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8649852317119553, + "yMeters": 3.0069610719378965, + "headingRadians": -0.062499999999998661, + "unwrappedHeadingRadians": -0.062499999999998661, + "arcLengthMeters": 6.8500000000000316, + "direction": 0, + "vehicleCurvaturePerMeter": 0.41666666666666663, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 20, + "startsAtGearSwitch": false, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 1, + "direction": 1, + "startIndex": 21, + "endIndex": 61, + "startsAtGearSwitch": true, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 2, + "direction": 0, + "startIndex": 62, + "endIndex": 276, + "startsAtGearSwitch": true, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "narrow-corridor", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:5ec976b32eec84abdd781b8cedeb21d07dfd6809ea61c108bcce8cff0b9d3a37", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [ + { + "kind": "axis-aligned-rectangle", + "xMinMm": 1500.0, + "xMaxMm": 4500.0, + "yMinMm": 500.0, + "yMaxMm": 1200.0, + "centerXMm": 0.0, + "centerYMm": 0.0, + "radiusMm": 0.0 + }, + { + "kind": "axis-aligned-rectangle", + "xMinMm": 1500.0, + "xMaxMm": 4500.0, + "yMinMm": 2800.0, + "yMaxMm": 3500.0, + "centerXMm": 0.0, + "centerYMm": 0.0, + "radiusMm": 0.0 + } + ] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23384439355857134, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.025, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.024999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23384439355857134, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.049999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.23384439355857134, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.074999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.099999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.20920160933177634, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.12499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.18684887719411869, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.14999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.18684887719411869, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1749999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.17499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.16697633040195181, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1999999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.19999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.16697633040195181, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.2249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14977102437387113, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2499999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.24999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.14977102437387113, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.274999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.274999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2999999999999989, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.29999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.13541034414477748, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3249999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.32499999999999885, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.12405453637069408, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3499999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.34999999999999876, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.12405453637069408, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3749999999999987, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.37499999999999867, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.11583890685285414, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3999999999999986, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.39999999999999858, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.11583890685285414, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4249999999999985, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.42499999999999849, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.1108664282504217, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4499999999999984, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.4499999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.1108664282504217, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4749999999999983, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.47499999999999831, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4999999999999982, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.49999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5249999999999981, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.52499999999999813, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.549999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.549999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.574999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.574999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5999999999999979, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.59999999999999787, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6249999999999978, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.62499999999999778, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6499999999999977, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.64999999999999769, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6749999999999976, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.6749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6999999999999975, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.69999999999999751, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7249999999999974, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.72499999999999742, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7499999999999973, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.74999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7749999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.77499999999999725, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7999999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.79999999999999716, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8249999999999971, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.82499999999999707, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.849999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.849999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8749999999999969, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.87499999999999689, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8999999999999968, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.8999999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9249999999999967, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.92499999999999671, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9499999999999966, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.94999999999999662, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9749999999999965, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.97499999999999654, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9999999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.99999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0249999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0249999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0499999999999963, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0749999999999962, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0749999999999962, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0999999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0999999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.124999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.124999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1499999999999959, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1499999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1749999999999958, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1749999999999958, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1999999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1999999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2249999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2249999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2499999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2749999999999955, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2749999999999955, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2999999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2999999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3249999999999953, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3249999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3499999999999952, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3499999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3749999999999951, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3749999999999951, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4249999999999949, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4249999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4499999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4749999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4749999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4999999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5249999999999946, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5249999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5499999999999945, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5499999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5749999999999944, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5749999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5999999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5999999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6249999999999942, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6249999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6499999999999941, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6499999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.674999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.674999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.699999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.699999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7249999999999939, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7249999999999939, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7499999999999938, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7749999999999937, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7749999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8249999999999935, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8249999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8499999999999934, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8499999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8749999999999933, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8749999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8999999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9249999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9249999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9499999999999931, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9499999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.974999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.974999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9999999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0249999999999928, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0249999999999928, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0499999999999927, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0499999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0749999999999926, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0749999999999926, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0999999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0999999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1499999999999924, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1499999999999924, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1749999999999923, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1749999999999923, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2249999999999921, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2249999999999921, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2749999999999919, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2749999999999919, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2999999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2999999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3249999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3249999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3499999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3499999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3749999999999916, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3749999999999916, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3999999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3999999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4249999999999914, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4249999999999914, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4499999999999913, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4499999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4749999999999912, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4749999999999912, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4999999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.524999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.524999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5499999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5499999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5749999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5749999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6249999999999907, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6249999999999907, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6499999999999906, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6499999999999906, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6749999999999905, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6749999999999905, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6999999999999904, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6999999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7249999999999903, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7249999999999903, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7499999999999902, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7499999999999902, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.77499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.77499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.79999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.79999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.82499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.82499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.84999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.84999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.87499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.87499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8999999999999897, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.8999999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9249999999999896, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9249999999999896, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9499999999999895, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9499999999999895, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9749999999999894, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9749999999999894, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9999999999999893, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.02499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0249999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.04999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.04999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.07499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0749999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1249999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1249999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1499999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1499999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1749999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1749999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2499999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2499999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2749999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2749999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.324999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.324999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3499999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3499999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3749999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3749999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4249999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4249999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4499999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4499999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4749999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4749999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4999999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4999999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5249999999999959, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5249999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5499999999999963, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.5749999999999966, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5749999999999966, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.10920160933177625, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.599999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.599999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.1108664282504217, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6249999999999973, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6249999999999973, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 0.1108664282504217, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6499999999999977, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6499999999999977, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.674999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.674999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.6999999999999984, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6999999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7249999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7249999999999988, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7499999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7499999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.7749999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7749999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.825, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.825, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.8500000000000005, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8500000000000005, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 6.5704775019876669, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 154, + "startsAtGearSwitch": false, + "endsAtGearSwitch": false + } + ] + }, + { + "id": "forward-reverse-switch", + "fixtureVersion": 1, + "configurationFingerprint": "sha256:86e1436370027ad30a7a205a78da291f81345e6acff2c0bb4f2256803ad9d962", + "map": { + "xMinMm": 0.0, + "xMaxMm": 6000.0, + "yMinMm": 0.0, + "yMaxMm": 4000.0, + "resolutionMm": 50.0, + "obstacles": [] + }, + "vehicle": { + "lengthMeters": 0.8, + "widthMeters": 0.6, + "safetyMarginMeters": 0.05, + "maximumCurvaturePerMeter": 0.83333333333333337 + }, + "planningConfiguration": { + "primitiveLengthMeters": 0.5, + "integrationStepMeters": 0.05, + "maximumCollisionCheckStepMeters": 0.025, + "headingResolutionRadians": 0.087266462599716474, + "curvatureLevelCount": 5, + "goalPositionToleranceMeters": 0.15, + "goalHeadingToleranceRadians": 0.087266462599716474, + "maximumExpandedNodes": 200000, + "searchTimeoutSeconds": 120.0, + "heuristicWeight": 1.0, + "reverseCostMultiplier": 1.5, + "gearSwitchPenaltyMeters": 1.0, + "curvatureMagnitudeWeight": 0.1, + "curvatureChangePenaltyMetersPerLevel": 0.05, + "clearanceCostWeight": 0.2, + "clearanceCostDistanceMeters": 0.5, + "allowReverse": true + }, + "path": [ + { + "xMeters": 1.0, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.0, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 0 + }, + { + "xMeters": 1.025, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.024999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0499999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.049999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0749999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.074999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.0999999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.099999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1249999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.12499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1499999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.14999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1749999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.17499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.1999999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.19999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.2249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2499999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.24999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.274999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.274999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.2999999999999989, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.29999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3249999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.32499999999999885, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3499999999999988, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.34999999999999876, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3749999999999987, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.37499999999999867, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.3999999999999986, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.39999999999999858, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4249999999999985, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.42499999999999849, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4499999999999984, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.4499999999999984, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4749999999999983, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.47499999999999831, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.4999999999999982, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.49999999999999822, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5249999999999981, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.52499999999999813, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.549999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.549999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.574999999999998, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.574999999999998, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.5999999999999979, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.59999999999999787, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6249999999999978, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.62499999999999778, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6499999999999977, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.64999999999999769, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6749999999999976, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.6749999999999976, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.6999999999999975, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.69999999999999751, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7249999999999974, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.72499999999999742, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7499999999999973, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.74999999999999734, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7749999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.77499999999999725, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.7999999999999972, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.79999999999999716, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8249999999999971, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.82499999999999707, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.849999999999997, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.849999999999997, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8749999999999969, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.87499999999999689, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.8999999999999968, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.8999999999999968, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9249999999999967, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.92499999999999671, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9499999999999966, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.94999999999999662, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9749999999999965, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.97499999999999654, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 1.9999999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 0.99999999999999645, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0249999999999964, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0249999999999964, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0499999999999963, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0499999999999963, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0749999999999962, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0749999999999962, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.0999999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.0999999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.124999999999996, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.124999999999996, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1499999999999959, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1499999999999959, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1749999999999958, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1749999999999958, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.1999999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.1999999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2249999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2249999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2499999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2499999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2749999999999955, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2749999999999955, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.2999999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.2999999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3249999999999953, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3249999999999953, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3499999999999952, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3499999999999952, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.3749999999999951, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.3749999999999951, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4249999999999949, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4249999999999949, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4499999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4499999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4749999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4749999999999948, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.4999999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.4999999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5249999999999946, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5249999999999946, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5499999999999945, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5499999999999945, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5749999999999944, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5749999999999944, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.5999999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.5999999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6249999999999942, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6249999999999942, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.6499999999999941, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.6499999999999941, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.674999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.674999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.699999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.699999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7249999999999939, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7249999999999939, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7499999999999938, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7499999999999938, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7749999999999937, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7749999999999937, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.7999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.7999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8249999999999935, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8249999999999935, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8499999999999934, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8499999999999934, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8749999999999933, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8749999999999933, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.8999999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.8999999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9249999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9249999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9499999999999931, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9499999999999931, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.974999999999993, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.974999999999993, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 2.9999999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 1.9999999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0249999999999928, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0249999999999928, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0499999999999927, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0499999999999927, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0749999999999926, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0749999999999926, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.0999999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.0999999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1499999999999924, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1499999999999924, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1749999999999923, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1749999999999923, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2249999999999921, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2249999999999921, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.249999999999992, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2749999999999919, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2749999999999919, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.2999999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.2999999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3249999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3249999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3499999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3499999999999917, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3749999999999916, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3749999999999916, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.3999999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.3999999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4249999999999914, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4249999999999914, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4499999999999913, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4499999999999913, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4749999999999912, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4749999999999912, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.4999999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.4999999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.524999999999991, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.524999999999991, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5499999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5499999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5749999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5749999999999909, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.5999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.5999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6249999999999907, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6249999999999907, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6499999999999906, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6499999999999906, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6749999999999905, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6749999999999905, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.6999999999999904, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.6999999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7249999999999903, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7249999999999903, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.7499999999999902, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.7499999999999902, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.77499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.77499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.79999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.79999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.82499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.82499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.84999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.84999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.87499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.87499999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.8999999999999897, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.8999999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9249999999999896, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9249999999999896, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9499999999999895, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9499999999999895, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9749999999999894, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9749999999999894, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 3.9999999999999893, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 2.9999999999999893, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.02499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0249999999999897, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.04999999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.04999999999999, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.07499999999999, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0749999999999904, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.0999999999999908, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.0999999999999908, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1249999999999911, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1249999999999911, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1499999999999915, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1499999999999915, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1749999999999918, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1749999999999918, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1999999999999922, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.1999999999999922, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2249999999999925, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2249999999999925, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2499999999999929, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2499999999999929, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2749999999999932, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2749999999999932, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2999999999999936, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.2999999999999936, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.324999999999994, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.324999999999994, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3499999999999943, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3499999999999943, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3749999999999947, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.3749999999999947, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.399999999999995, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.399999999999995, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4249999999999954, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4249999999999954, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4499999999999957, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4499999999999957, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4749999999999961, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4749999999999961, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4999999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4999999999999956, + "direction": 0, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4999999999999956, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.4999999999999956, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": true, + "source": 1 + }, + { + "xMeters": 4.4749999999999952, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5249999999999959, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4499999999999948, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5499999999999963, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.4249999999999945, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.5749999999999966, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3999999999999941, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.599999999999997, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3749999999999938, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6249999999999973, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3499999999999934, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6499999999999977, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.3249999999999931, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.674999999999998, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2999999999999927, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.6999999999999984, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2749999999999924, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7249999999999988, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.249999999999992, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7499999999999991, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.2249999999999917, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.7749999999999995, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1999999999999913, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1749999999999909, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.825, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 1 + }, + { + "xMeters": 4.1499999999999906, + "yMeters": 2.0, + "headingRadians": 0.0, + "unwrappedHeadingRadians": 0.0, + "arcLengthMeters": 3.8500000000000005, + "direction": 1, + "vehicleCurvaturePerMeter": 0.0, + "bodyClearanceMeters": 7.2111025509279782, + "isGearSwitchPoint": false, + "source": 2 + } + ], + "segments": [ + { + "segmentIndex": 0, + "direction": 0, + "startIndex": 0, + "endIndex": 140, + "startsAtGearSwitch": false, + "endsAtGearSwitch": true + }, + { + "segmentIndex": 1, + "direction": 1, + "startIndex": 141, + "endIndex": 155, + "startsAtGearSwitch": true, + "endsAtGearSwitch": false + } + ] + } + ] +} \ No newline at end of file diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs new file mode 100644 index 0000000..2d564be --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +/// 开发人员离线导出八个快速夹具和四个 Hybrid A* 端到端场景的比较报告。 +public sealed class PathSmoothingComparisonDemo +{ + private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); + private readonly PathSmoothingComparisonService _comparisonService = new PathSmoothingComparisonService(); + private readonly SmoothingFigureModelBuilder _figureBuilder = new SmoothingFigureModelBuilder(); + private readonly SmoothingReportExporter _reportExporter = new SmoothingReportExporter(); + + /// 导出八个已验证夹具;不运行 Hybrid A*。 + public IReadOnlyList ExportFixtureReports( + string fixturePath, + string outputDirectory, + CancellationToken cancellationToken = default) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); + var results = new List(requests.Count); + for (int index = 0; index < requests.Count; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + results.Add(Export(fixtures[index].Id, requests[index], outputDirectory, cancellationToken)); + } + return results.AsReadOnly(); + } + + /// 运行四个固定 Hybrid A* 场景,并仅为成功粗路径导出平滑比较报告。 + public IReadOnlyList ExportEndToEndReports( + string outputDirectory, + CancellationToken cancellationToken = default) + { + CoarsePathTestScenario[] scenarios = + { + CoarsePathTestScenario.ExplicitEmpty, + CoarsePathTestScenario.RectangleDetour, + CoarsePathTestScenario.ManualAndTwoLeg, + CoarsePathTestScenario.ReverseGearSwitch, + }; + var planner = new CoarsePathPlanningService(); + var results = new List(scenarios.Length); + for (int index = 0; index < scenarios.Length; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenarios[index]); + CoarsePathPlanningJobResult coarse = planner.Plan(job, cancellationToken); + if (coarse.PlanningResult.Status != PlanningStatus.Success) + { + results.Add(PathSmoothingComparisonScenarioResult.CoarsePathFailure( + scenarios[index].ToString(), coarse.PlanningResult.Status, coarse.PlanningResult.Diagnostics.TerminationReason)); + continue; + } + results.Add(Export( + scenarios[index].ToString(), + SmoothingScenarioFactory.CreateEndToEndRequest(job, coarse), + outputDirectory, + cancellationToken)); + } + return results.AsReadOnly(); + } + + /// 供业务调用示例使用:仅在已成功的粗路径上调用平滑服务。 + public PathSmoothingResult SmoothSuccessfulCoarsePath( + CoarsePathPlanningJob job, + CoarsePathPlanningJobResult coarseResult, + PathSmoothingConfiguration configuration, + CancellationToken cancellationToken = default) + { + if (job == null || coarseResult == null || coarseResult.PlanningResult.Status != PlanningStatus.Success) + throw new ArgumentException("只有成功的粗路径能够进入平滑流程。", nameof(coarseResult)); + return _smoothingService.Smooth(new PathSmoothingRequest( + coarseResult.PlanningResult.Path, + coarseResult.PlanningResult.Segments, + coarseResult.MapResult.Map, + job.Vehicle, + configuration), cancellationToken); + } + + /// 业务调用方只可消费成功平滑或显式回退的路径。 + public static bool IsConsumable(PathSmoothingStatus status) + { + return status == PathSmoothingStatus.Success || status == PathSmoothingStatus.FallbackToCoarsePath; + } + + private PathSmoothingComparisonScenarioResult Export( + string scenarioId, + PathSmoothingComparisonRequest request, + string outputDirectory, + CancellationToken cancellationToken) + { + if (request == null || request.SmoothingRequest == null || request.SmoothingRequest.CoarsePath.Count == 0) + throw new ArgumentException("比较请求必须包含粗路径。", nameof(request)); + PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + SmoothingFigureModel model = _figureBuilder.Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + scenarioId, + scenarioId); + SmoothingReportExportResult report = _reportExporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = System.IO.Path.Combine(outputDirectory, scenarioId), + FileStem = "comparison", + }); + return PathSmoothingComparisonScenarioResult.ComparisonComplete(scenarioId, comparison, report); + } +} + +/// 批处理为一个场景发布的粗路径状态、平滑比较和报告结果。 +public sealed class PathSmoothingComparisonScenarioResult +{ + private PathSmoothingComparisonScenarioResult( + string scenarioId, + PlanningStatus coarsePathStatus, + string diagnostic, + PathSmoothingComparisonResult comparison, + SmoothingReportExportResult report) + { + ScenarioId = scenarioId ?? string.Empty; + CoarsePathStatus = coarsePathStatus; + Diagnostic = diagnostic ?? string.Empty; + Comparison = comparison; + Report = report; + } + + public string ScenarioId { get; } + public PlanningStatus CoarsePathStatus { get; } + public string Diagnostic { get; } + public PathSmoothingComparisonResult Comparison { get; } + public SmoothingReportExportResult Report { get; } + + internal static PathSmoothingComparisonScenarioResult CoarsePathFailure( + string scenarioId, + PlanningStatus status, + string diagnostic) + { + return new PathSmoothingComparisonScenarioResult(scenarioId, status, diagnostic, null, null); + } + + internal static PathSmoothingComparisonScenarioResult ComparisonComplete( + string scenarioId, + PathSmoothingComparisonResult comparison, + SmoothingReportExportResult report) + { + return new PathSmoothingComparisonScenarioResult(scenarioId, PlanningStatus.Success, string.Empty, comparison, report); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs new file mode 100644 index 0000000..c942216 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingFixtureGenerator.cs @@ -0,0 +1,260 @@ +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; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs new file mode 100644 index 0000000..f263573 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +/// 将快速夹具或现有粗路径业务结果转换为独立平滑比较请求。 +public static class SmoothingScenarioFactory +{ + /// 不运行 Hybrid A*,从已验证的 JSON 夹具创建八个快速比较请求。 + public static IReadOnlyList CreateFixtureRequests(string fixturePath) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + var requests = new List(fixtures.Count); + for (int index = 0; index < fixtures.Count; index++) + { + SmoothingScenarioFixture fixture = fixtures[index]; + requests.Add(new PathSmoothingComparisonRequest( + new PathSmoothingRequest( + fixture.Path, + fixture.Segments, + SmoothingScenarioFixtureLoader.BuildMap(fixture), + SmoothingScenarioFixtureLoader.BuildVehicle(fixture), + new PathSmoothingConfiguration()))); + } + return requests.AsReadOnly(); + } + + /// 把现有 已规划成功的结果转换为比较请求。 + public static PathSmoothingComparisonRequest CreateEndToEndRequest( + CoarsePathPlanningJob job, + CoarsePathPlanningJobResult result) + { + if (job == null || result == null || result.MapResult == null || result.MapResult.Map == null || + result.PlanningResult == null || result.PlanningResult.Status != PlanningStatus.Success) + throw new ArgumentException("只能从包含成功地图和粗路径的业务结果创建平滑比较请求。", nameof(result)); + + return new PathSmoothingComparisonRequest(new PathSmoothingRequest( + CopyWithFiniteClearance(result.PlanningResult.Path, result.MapResult.Map), + result.PlanningResult.Segments, + result.MapResult.Map, + job.Vehicle, + new PathSmoothingConfiguration())); + } + + internal static IReadOnlyList CopyWithFiniteClearance( + IReadOnlyList source, + MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap map) + { + double widthMeters = (map.Bounds.XMax - map.Bounds.XMin) / 1000d; + double heightMeters = (map.Bounds.YMax - map.Bounds.YMin) / 1000d; + double finiteEmptyMapClearance = Math.Sqrt(widthMeters * widthMeters + heightMeters * heightMeters); + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + { + CoarsePathPoint point = source[index]; + double clearance = double.IsPositiveInfinity(point.BodyClearance) + ? finiteEmptyMapClearance + : point.BodyClearance; + copy.Add(new CoarsePathPoint( + point.X, point.Y, point.Heading, point.UnwrappedHeading, point.ArcLength, point.Direction, + point.VehicleCurvature, clearance, point.IsGearSwitchPoint, point.Source)); + } + return copy.AsReadOnly(); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs new file mode 100644 index 0000000..e19185d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixture.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using Newtonsoft.Json; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +/// 一个可复现的快速粗路径夹具及其地图、车辆和方向段快照。 +public sealed class SmoothingScenarioFixture +{ + internal SmoothingScenarioFixture(SmoothingFixtureRecord record, bool fingerprintCurrent) + { + Record = record; + Id = record.Id; + FixtureVersion = record.FixtureVersion; + ConfigurationFingerprint = record.ConfigurationFingerprint; + IsConfigurationFingerprintCurrent = fingerprintCurrent; + Path = ToPath(record.Path); + Segments = ToSegments(record.Segments); + } + + /// 稳定英文场景标识。 + public string Id { get; } + /// 夹具自身的正版本号。 + public int FixtureVersion { get; } + /// 由夹具输入生成的稳定 SHA-256 指纹。 + public string ConfigurationFingerprint { get; } + /// 存储指纹是否与当前夹具内容一致。 + public bool IsConfigurationFingerprintCurrent { get; } + /// 快速比较使用的不可变粗路径点。 + public IReadOnlyList Path { get; } + /// 覆盖粗路径的不可变方向段。 + public IReadOnlyList Segments { get; } + + internal SmoothingFixtureRecord Record { get; } + + private static IReadOnlyList ToPath(IReadOnlyList source) + { + var result = new List(source.Count); + for (int index = 0; index < source.Count; index++) + { + SmoothingFixturePathPoint point = source[index]; + result.Add(new CoarsePathPoint( + point.XMeters, point.YMeters, point.HeadingRadians, point.UnwrappedHeadingRadians, + point.ArcLengthMeters, point.Direction, point.VehicleCurvaturePerMeter, + point.BodyClearanceMeters, point.IsGearSwitchPoint, point.Source)); + } + return new ReadOnlyCollection(result); + } + + private static IReadOnlyList ToSegments(IReadOnlyList source) + { + var result = new List(source.Count); + for (int index = 0; index < source.Count; index++) + { + SmoothingFixtureSegment segment = source[index]; + result.Add(new PathSegment(segment.SegmentIndex, segment.Direction, segment.StartIndex, segment.EndIndex, + segment.StartsAtGearSwitch, segment.EndsAtGearSwitch)); + } + return new ReadOnlyCollection(result); + } +} + +internal sealed class SmoothingFixtureDocument +{ + [JsonProperty("schemaVersion")] public int SchemaVersion { get; set; } + [JsonProperty("scenarios")] public List Scenarios { get; set; } = new List(); +} + +internal sealed class SmoothingFixtureRecord +{ + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("fixtureVersion")] public int FixtureVersion { get; set; } + [JsonProperty("configurationFingerprint")] public string ConfigurationFingerprint { get; set; } + [JsonProperty("map")] public SmoothingFixtureMap Map { get; set; } + [JsonProperty("vehicle")] public SmoothingFixtureVehicle Vehicle { get; set; } + [JsonProperty("planningConfiguration")] public SmoothingFixturePlanningConfiguration PlanningConfiguration { get; set; } + [JsonProperty("path")] public List Path { get; set; } = new List(); + [JsonProperty("segments")] public List Segments { get; set; } = new List(); +} + +internal sealed class SmoothingFixtureMap +{ + [JsonProperty("xMinMm")] public float XMinMm { get; set; } + [JsonProperty("xMaxMm")] public float XMaxMm { get; set; } + [JsonProperty("yMinMm")] public float YMinMm { get; set; } + [JsonProperty("yMaxMm")] public float YMaxMm { get; set; } + [JsonProperty("resolutionMm")] public float ResolutionMm { get; set; } + [JsonProperty("obstacles")] public List Obstacles { get; set; } = new List(); +} + +internal sealed class SmoothingFixtureObstacle +{ + [JsonProperty("kind")] public string Kind { get; set; } + [JsonProperty("xMinMm")] public float XMinMm { get; set; } + [JsonProperty("xMaxMm")] public float XMaxMm { get; set; } + [JsonProperty("yMinMm")] public float YMinMm { get; set; } + [JsonProperty("yMaxMm")] public float YMaxMm { get; set; } + [JsonProperty("centerXMm")] public float CenterXMm { get; set; } + [JsonProperty("centerYMm")] public float CenterYMm { get; set; } + [JsonProperty("radiusMm")] public float RadiusMm { get; set; } +} + +internal sealed class SmoothingFixtureVehicle +{ + [JsonProperty("lengthMeters")] public double LengthMeters { get; set; } + [JsonProperty("widthMeters")] public double WidthMeters { get; set; } + [JsonProperty("safetyMarginMeters")] public double SafetyMarginMeters { get; set; } + [JsonProperty("maximumCurvaturePerMeter")] public double MaximumCurvaturePerMeter { get; set; } +} + +/// 产生夹具粗路径时使用的 Hybrid A* 配置快照;仅用于溯源和指纹验证,不参与快速夹具加载时的规划。 +internal sealed class SmoothingFixturePlanningConfiguration +{ + [JsonProperty("primitiveLengthMeters")] public double PrimitiveLengthMeters { get; set; } + [JsonProperty("integrationStepMeters")] public double IntegrationStepMeters { get; set; } + [JsonProperty("maximumCollisionCheckStepMeters")] public double MaximumCollisionCheckStepMeters { get; set; } + [JsonProperty("headingResolutionRadians")] public double HeadingResolutionRadians { get; set; } + [JsonProperty("curvatureLevelCount")] public int CurvatureLevelCount { get; set; } + [JsonProperty("goalPositionToleranceMeters")] public double GoalPositionToleranceMeters { get; set; } + [JsonProperty("goalHeadingToleranceRadians")] public double GoalHeadingToleranceRadians { get; set; } + [JsonProperty("maximumExpandedNodes")] public int MaximumExpandedNodes { get; set; } + [JsonProperty("searchTimeoutSeconds")] public double SearchTimeoutSeconds { get; set; } + [JsonProperty("heuristicWeight")] public double HeuristicWeight { get; set; } + [JsonProperty("reverseCostMultiplier")] public double ReverseCostMultiplier { get; set; } + [JsonProperty("gearSwitchPenaltyMeters")] public double GearSwitchPenaltyMeters { get; set; } + [JsonProperty("curvatureMagnitudeWeight")] public double CurvatureMagnitudeWeight { get; set; } + [JsonProperty("curvatureChangePenaltyMetersPerLevel")] public double CurvatureChangePenaltyMetersPerLevel { get; set; } + [JsonProperty("clearanceCostWeight")] public double ClearanceCostWeight { get; set; } + [JsonProperty("clearanceCostDistanceMeters")] public double ClearanceCostDistanceMeters { get; set; } + [JsonProperty("allowReverse")] public bool AllowReverse { get; set; } +} + +internal sealed class SmoothingFixturePathPoint +{ + [JsonProperty("xMeters")] public double XMeters { get; set; } + [JsonProperty("yMeters")] public double YMeters { get; set; } + [JsonProperty("headingRadians")] public double HeadingRadians { get; set; } + [JsonProperty("unwrappedHeadingRadians")] public double UnwrappedHeadingRadians { get; set; } + [JsonProperty("arcLengthMeters")] public double ArcLengthMeters { get; set; } + [JsonProperty("direction")] public TravelDirection Direction { get; set; } + [JsonProperty("vehicleCurvaturePerMeter")] public double VehicleCurvaturePerMeter { get; set; } + [JsonProperty("bodyClearanceMeters")] public double BodyClearanceMeters { get; set; } + [JsonProperty("isGearSwitchPoint")] public bool IsGearSwitchPoint { get; set; } + [JsonProperty("source")] public CoarsePathPointSource Source { get; set; } +} + +internal sealed class SmoothingFixtureSegment +{ + [JsonProperty("segmentIndex")] public int SegmentIndex { get; set; } + [JsonProperty("direction")] public TravelDirection Direction { get; set; } + [JsonProperty("startIndex")] public int StartIndex { get; set; } + [JsonProperty("endIndex")] public int EndIndex { get; set; } + [JsonProperty("startsAtGearSwitch")] public bool StartsAtGearSwitch { get; set; } + [JsonProperty("endsAtGearSwitch")] public bool EndsAtGearSwitch { get; set; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs new file mode 100644 index 0000000..e66b4e9 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using Newtonsoft.Json; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +/// 加载并验证版本化 JSON 快速夹具;此类不运行 Hybrid A*。 +public static class SmoothingScenarioFixtureLoader +{ + /// 读取、校验并冻结指定的快速夹具文件。 + public static IReadOnlyList LoadAndVerify(string fixturePath) + { + if (string.IsNullOrWhiteSpace(fixturePath)) throw new ArgumentException("夹具路径不能为空。", nameof(fixturePath)); + if (!File.Exists(fixturePath)) throw new FileNotFoundException("找不到路径平滑夹具文件。", fixturePath); + + SmoothingFixtureDocument document = JsonConvert.DeserializeObject( + File.ReadAllText(fixturePath, Encoding.UTF8)); + if (document == null || document.SchemaVersion != 1 || document.Scenarios == null) + throw new InvalidDataException("路径平滑夹具架构版本无效。"); + + var fixtures = new List(document.Scenarios.Count); + var ids = new HashSet(StringComparer.Ordinal); + for (int index = 0; index < document.Scenarios.Count; index++) + { + SmoothingFixtureRecord record = document.Scenarios[index]; + Validate(record, ids); + string fingerprint = ComputeFingerprint(record); + if (!string.Equals(record.ConfigurationFingerprint, fingerprint, StringComparison.Ordinal)) + throw new InvalidDataException("路径平滑夹具指纹已过期:" + record.Id + "。"); + fixtures.Add(new SmoothingScenarioFixture(record, true)); + } + return fixtures.AsReadOnly(); + } + + internal static string ComputeFingerprint(SmoothingFixtureRecord record) + { + string material = BuildFingerprintMaterial(record); + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(material)); + var builder = new StringBuilder(hash.Length * 2 + 7); + builder.Append("sha256:"); + for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2", CultureInfo.InvariantCulture)); + return builder.ToString(); + } + } + + internal static PlanningGridMap BuildMap(SmoothingScenarioFixture fixture) + { + SmoothingFixtureRecord record = fixture.Record; + var obstacles = new List(record.Map.Obstacles.Count); + for (int index = 0; index < record.Map.Obstacles.Count; index++) + { + SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index]; + if (string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal)) + obstacles.Add(new CircleObstacle(obstacle.CenterXMm, obstacle.CenterYMm, obstacle.RadiusMm)); + else if (string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal)) + obstacles.Add(new AxisAlignedRectangleObstacle(obstacle.XMinMm, obstacle.XMaxMm, obstacle.YMinMm, obstacle.YMaxMm)); + else + throw new InvalidDataException("夹具包含未知障碍物类型。"); + } + + var mapRequest = new PlanningMapRequest + { + Bounds = new MapBoundsMm(record.Map.XMinMm, record.Map.XMaxMm, record.Map.YMinMm, record.Map.YMaxMm), + ResolutionMm = record.Map.ResolutionMm, + AllowExplicitEmptyMap = obstacles.Count == 0, + ObstacleSources = obstacles.Count == 0 + ? Array.Empty() + : new IMapObstacleSource[] { new ManualObstacleSource("fixture-" + record.Id, record.FixtureVersion, true, obstacles) }, + }; + PlanningMapBuildResult build = new PlanningMapFactory().Create(mapRequest); + if (!build.Succeeded || build.Map == null) throw new InvalidDataException("夹具地图无法重建:" + record.Id + "。"); + return build.Map; + } + + internal static VehicleParameters BuildVehicle(SmoothingScenarioFixture fixture) + { + SmoothingFixtureVehicle vehicle = fixture.Record.Vehicle; + return new VehicleParameters + { + LengthMeters = vehicle.LengthMeters, + WidthMeters = vehicle.WidthMeters, + SafetyMarginMeters = vehicle.SafetyMarginMeters, + MaximumCurvaturePerMeter = vehicle.MaximumCurvaturePerMeter, + }; + } + + private static void Validate(SmoothingFixtureRecord record, ISet ids) + { + if (record == null || string.IsNullOrWhiteSpace(record.Id) || record.FixtureVersion <= 0 || + record.Map == null || record.Vehicle == null || record.PlanningConfiguration == null || record.Path == null || record.Path.Count < 2 || + record.Segments == null || record.Segments.Count == 0 || !ids.Add(record.Id)) + throw new InvalidDataException("路径平滑夹具缺少必需字段、版本或唯一 ID。"); + + ValidateMap(record); + ValidateVehicle(record); + ValidatePlanningConfiguration(record); + ValidatePathContract(record); + } + + private static void ValidateMap(SmoothingFixtureRecord record) + { + SmoothingFixtureMap map = record.Map; + if (!IsFinite(map.XMinMm) || !IsFinite(map.XMaxMm) || !IsFinite(map.YMinMm) || !IsFinite(map.YMaxMm) || + !IsFinite(map.ResolutionMm) || map.XMaxMm <= map.XMinMm || map.YMaxMm <= map.YMinMm || map.ResolutionMm <= 0f || + map.Obstacles == null) + { + throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + "."); + } + + for (int index = 0; index < map.Obstacles.Count; index++) + { + SmoothingFixtureObstacle obstacle = map.Obstacles[index]; + bool circle = obstacle != null && string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal) && + IsFinite(obstacle.CenterXMm) && IsFinite(obstacle.CenterYMm) && IsFinite(obstacle.RadiusMm) && obstacle.RadiusMm >= 0f; + bool rectangle = obstacle != null && string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal) && + IsFinite(obstacle.XMinMm) && IsFinite(obstacle.XMaxMm) && IsFinite(obstacle.YMinMm) && IsFinite(obstacle.YMaxMm) && + obstacle.XMaxMm >= obstacle.XMinMm && obstacle.YMaxMm >= obstacle.YMinMm; + if (!circle && !rectangle) + throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + "."); + } + } + + private static void ValidateVehicle(SmoothingFixtureRecord record) + { + SmoothingFixtureVehicle vehicle = record.Vehicle; + if (!IsFinite(vehicle.LengthMeters) || !IsFinite(vehicle.WidthMeters) || !IsFinite(vehicle.SafetyMarginMeters) || + !IsFinite(vehicle.MaximumCurvaturePerMeter) || vehicle.LengthMeters <= 0d || vehicle.WidthMeters <= 0d || + vehicle.SafetyMarginMeters < 0d || vehicle.MaximumCurvaturePerMeter <= 0d) + { + throw new InvalidDataException("Fixture vehicle contract is invalid: " + record.Id + "."); + } + } + + private static void ValidatePlanningConfiguration(SmoothingFixtureRecord record) + { + SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration; + if (!IsPositiveFinite(configuration.PrimitiveLengthMeters) || !IsPositiveFinite(configuration.IntegrationStepMeters) || + !IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || !IsPositiveFinite(configuration.HeadingResolutionRadians) || + configuration.CurvatureLevelCount < 2 || !IsPositiveFinite(configuration.GoalPositionToleranceMeters) || + !IsPositiveFinite(configuration.GoalHeadingToleranceRadians) || configuration.MaximumExpandedNodes <= 0 || + !IsPositiveFinite(configuration.SearchTimeoutSeconds) || !IsFinite(configuration.HeuristicWeight) || + !IsFinite(configuration.ReverseCostMultiplier) || !IsFinite(configuration.GearSwitchPenaltyMeters) || + !IsFinite(configuration.CurvatureMagnitudeWeight) || !IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) || + !IsFinite(configuration.ClearanceCostWeight) || !IsPositiveFinite(configuration.ClearanceCostDistanceMeters)) + { + throw new InvalidDataException("Fixture planning configuration contract is invalid: " + record.Id + "."); + } + } + + private static void ValidatePathContract(SmoothingFixtureRecord record) + { + var fixture = new SmoothingScenarioFixture(record, true); + try + { + var request = new PathSmoothingRequest( + fixture.Path, + fixture.Segments, + BuildMap(fixture), + BuildVehicle(fixture), + new PathSmoothingConfiguration()); + if (!new PathSmoothingPreprocessor().TryPrepare(request, out _, out string reason)) + throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + reason); + } + catch (InvalidDataException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + exception.Message, exception); + } + } + + private static bool IsFinite(float value) + { + return !float.IsNaN(value) && !float.IsInfinity(value); + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + + private static bool IsPositiveFinite(double value) + { + return IsFinite(value) && value > 0d; + } + + private static string BuildFingerprintMaterial(SmoothingFixtureRecord record) + { + var builder = new StringBuilder(); + Append(builder, record.Id); Append(builder, record.FixtureVersion); + Append(builder, record.Map.XMinMm); Append(builder, record.Map.XMaxMm); Append(builder, record.Map.YMinMm); + Append(builder, record.Map.YMaxMm); Append(builder, record.Map.ResolutionMm); + for (int index = 0; index < record.Map.Obstacles.Count; index++) + { + SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index]; + Append(builder, obstacle.Kind); Append(builder, obstacle.XMinMm); Append(builder, obstacle.XMaxMm); + Append(builder, obstacle.YMinMm); Append(builder, obstacle.YMaxMm); Append(builder, obstacle.CenterXMm); + Append(builder, obstacle.CenterYMm); Append(builder, obstacle.RadiusMm); + } + Append(builder, record.Vehicle.LengthMeters); Append(builder, record.Vehicle.WidthMeters); + Append(builder, record.Vehicle.SafetyMarginMeters); Append(builder, record.Vehicle.MaximumCurvaturePerMeter); + SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration; + Append(builder, configuration.PrimitiveLengthMeters); Append(builder, configuration.IntegrationStepMeters); + Append(builder, configuration.MaximumCollisionCheckStepMeters); Append(builder, configuration.HeadingResolutionRadians); + Append(builder, configuration.CurvatureLevelCount); Append(builder, configuration.GoalPositionToleranceMeters); + Append(builder, configuration.GoalHeadingToleranceRadians); Append(builder, configuration.MaximumExpandedNodes); + Append(builder, configuration.SearchTimeoutSeconds); Append(builder, configuration.HeuristicWeight); + Append(builder, configuration.ReverseCostMultiplier); Append(builder, configuration.GearSwitchPenaltyMeters); + Append(builder, configuration.CurvatureMagnitudeWeight); Append(builder, configuration.CurvatureChangePenaltyMetersPerLevel); + Append(builder, configuration.ClearanceCostWeight); Append(builder, configuration.ClearanceCostDistanceMeters); + Append(builder, configuration.AllowReverse ? 1 : 0); + for (int index = 0; index < record.Path.Count; index++) + { + SmoothingFixturePathPoint point = record.Path[index]; + Append(builder, point.XMeters); Append(builder, point.YMeters); Append(builder, point.HeadingRadians); + Append(builder, point.UnwrappedHeadingRadians); Append(builder, point.ArcLengthMeters); Append(builder, (int)point.Direction); + Append(builder, point.VehicleCurvaturePerMeter); Append(builder, point.BodyClearanceMeters); + Append(builder, point.IsGearSwitchPoint ? 1 : 0); Append(builder, (int)point.Source); + } + for (int index = 0; index < record.Segments.Count; index++) + { + SmoothingFixtureSegment segment = record.Segments[index]; + Append(builder, segment.SegmentIndex); Append(builder, (int)segment.Direction); Append(builder, segment.StartIndex); + Append(builder, segment.EndIndex); Append(builder, segment.StartsAtGearSwitch ? 1 : 0); Append(builder, segment.EndsAtGearSwitch ? 1 : 0); + } + return builder.ToString(); + } + + private static void Append(StringBuilder builder, string value) { builder.Append(value ?? string.Empty).Append('|'); } + private static void Append(StringBuilder builder, int value) { builder.Append(value.ToString(CultureInfo.InvariantCulture)).Append('|'); } + private static void Append(StringBuilder builder, float value) + { + AppendBits(builder, BitConverter.GetBytes(value)); + } + + private static void Append(StringBuilder builder, double value) + { + AppendBits(builder, BitConverter.GetBytes(value)); + } + + private static void AppendBits(StringBuilder builder, byte[] bytes) + { + for (int index = bytes.Length - 1; index >= 0; index--) + builder.Append(bytes[index].ToString("x2", CultureInfo.InvariantCulture)); + builder.Append('|'); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs new file mode 100644 index 0000000..31fb88d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +/// 独立复核平滑候选的端点拓扑、车辆曲率和连续车体安全性。 +public sealed class SmoothedPathValidator +{ + private const double Tolerance = 1e-6d; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认连续车体碰撞检查器的平滑路径验证器。 + public SmoothedPathValidator() + : this(new FootprintCollisionChecker()) + { + } + + /// 创建使用指定连续车体碰撞检查器的平滑路径验证器。 + public SmoothedPathValidator(FootprintCollisionChecker collisionChecker) + { + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 复核候选平滑路径。每个候选方向段必须保持原始段的端点和换向拓扑; + /// 输出中的净空均由本次实际车体检查重新计算,绝不沿用候选声明值。 + /// + public bool TryValidate( + IReadOnlyList candidatePath, + IReadOnlyList candidateSegments, + PreparedPath originalPath, + PlanningGridMap map, + VehicleParameters vehicle, + double maximumCollisionCheckStepMeters, + out IReadOnlyList pathWithClearance, + out double minimumClearanceMeters, + out string reason) + { + pathWithClearance = EmptyPath(); + minimumClearanceMeters = 0d; + reason = string.Empty; + if (candidatePath == null || candidateSegments == null || originalPath == null || map == null || vehicle == null || + candidatePath.Count == 0 || candidateSegments.Count == 0 || !NumericGuard.IsPositiveFinite(maximumCollisionCheckStepMeters)) + { + reason = "平滑候选、原始路径、地图、车辆或碰撞步长无效。"; + return false; + } + + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvatureMeters)) + { + reason = "车辆曲率约束无效。"; + return false; + } + + if (!TryValidateSegmentTopology(candidatePath, candidateSegments, originalPath, out reason)) return false; + if (Math.Abs(candidatePath[0].ArcLength) > Tolerance) + { + reason = "平滑路径首点的全局弧长必须为零。"; + return false; + } + + var checkedClearances = new double[candidatePath.Count]; + double minimumClearance = double.PositiveInfinity; + for (int index = 0; index < candidatePath.Count; index++) + { + SmoothedPathPoint current = candidatePath[index]; + if (!IsValidPoint(current) || Math.Abs(current.VehicleCurvature) > maximumCurvatureMeters + Tolerance) + { + reason = "平滑路径包含非法数值或超限车辆曲率。"; + return false; + } + + var currentPose = new Pose2D(current.X, current.Y, current.Heading); + if (!_collisionChecker.IsPoseCollisionFree(currentPose, map, vehicle, 0d, out double poseClearance)) + { + reason = "平滑路径点未通过完整车体碰撞或边界复核。"; + return false; + } + + checkedClearances[index] = poseClearance; + minimumClearance = Math.Min(minimumClearance, poseClearance); + if (index == 0) continue; + + SmoothedPathPoint previous = candidatePath[index - 1]; + if (!IsUnwrappedHeadingContinuous(previous, current)) + { + reason = "平滑路径展开航向不连续。"; + return false; + } + + if (IsDuplicatePoseAndArcLength(previous, current)) + { + if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) + { + reason = "相邻重复点不是合法换向对。"; + return false; + } + continue; + } + + if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + Tolerance) + { + reason = "非换向点必须保持正弧长增量,换向点必须保留重复位姿。"; + return false; + } + + var previousPose = new Pose2D(previous.X, previous.Y, previous.Heading); + if (!_collisionChecker.IsSweptMotionCollisionFree(previousPose, currentPose, map, vehicle, + maximumCollisionCheckStepMeters, out double sweptClearance)) + { + reason = "平滑路径相邻点之间的完整车体扫掠碰撞复核失败。"; + return false; + } + + checkedClearances[index - 1] = Math.Min(checkedClearances[index - 1], sweptClearance); + checkedClearances[index] = Math.Min(checkedClearances[index], sweptClearance); + minimumClearance = Math.Min(minimumClearance, sweptClearance); + } + + var output = new List(candidatePath.Count); + for (int index = 0; index < candidatePath.Count; index++) + { + SmoothedPathPoint point = candidatePath[index]; + output.Add(new SmoothedPathPoint( + point.X, point.Y, point.Heading, point.UnwrappedHeading, point.ArcLength, point.Direction, + point.GeometricCurvature, point.VehicleCurvature, point.VehicleCurvatureDerivative, + checkedClearances[index], point.IsGearSwitchPoint, point.Source)); + } + + pathWithClearance = new ReadOnlyCollection(output); + minimumClearanceMeters = minimumClearance; + return true; + } + + private static bool TryValidateSegmentTopology( + IReadOnlyList candidatePath, + IReadOnlyList candidateSegments, + PreparedPath originalPath, + out string reason) + { + reason = string.Empty; + if (originalPath.Segments == null || originalPath.Segments.Count != candidateSegments.Count) + { + reason = "平滑路径方向段数量必须保持原始换向拓扑。"; + return false; + } + + int expectedStartIndex = 0; + for (int segmentIndex = 0; segmentIndex < candidateSegments.Count; segmentIndex++) + { + SmoothedPathSegment candidateSegment = candidateSegments[segmentIndex]; + PreparedDirectionSegment originalSegment = originalPath.Segments[segmentIndex]; + if (candidateSegment == null || originalSegment == null || candidateSegment.SegmentIndex != segmentIndex || + candidateSegment.StartIndex != expectedStartIndex || candidateSegment.StartIndex < 0 || + candidateSegment.EndIndex < candidateSegment.StartIndex || candidateSegment.EndIndex >= candidatePath.Count || + candidateSegment.Direction != originalSegment.Direction || + candidateSegment.StartsAtGearSwitch != originalSegment.StartsAtGearSwitch || + candidateSegment.EndsAtGearSwitch != originalSegment.EndsAtGearSwitch || + originalSegment.Points == null || originalSegment.Points.Count == 0) + { + reason = "平滑路径方向段索引、方向或换向拓扑无效。"; + return false; + } + + SmoothedPathPoint candidateStart = candidatePath[candidateSegment.StartIndex]; + SmoothedPathPoint candidateEnd = candidatePath[candidateSegment.EndIndex]; + SmoothingPoint2D originalStart = originalSegment.Points[0]; + SmoothingPoint2D originalEnd = originalSegment.Points[originalSegment.Points.Count - 1]; + if (!SamePose(candidateStart, originalStart) || !SamePose(candidateEnd, originalEnd) || + candidateStart.Direction != originalSegment.Direction || candidateEnd.Direction != originalSegment.Direction || + candidateStart.IsGearSwitchPoint != originalStart.IsGearSwitchPoint || + candidateEnd.IsGearSwitchPoint != originalEnd.IsGearSwitchPoint) + { + reason = "平滑路径改变了原始方向段端点或换向点。"; + return false; + } + + for (int pointIndex = candidateSegment.StartIndex; pointIndex <= candidateSegment.EndIndex; pointIndex++) + { + if (candidatePath[pointIndex] == null || candidatePath[pointIndex].Direction != originalSegment.Direction) + { + reason = "平滑路径方向段包含与段方向不一致的点。"; + return false; + } + } + + expectedStartIndex = candidateSegment.EndIndex + 1; + } + + if (expectedStartIndex != candidatePath.Count) + { + reason = "平滑路径方向段未完整覆盖候选点。"; + return false; + } + return true; + } + + private static bool IsValidPoint(SmoothedPathPoint point) + { + return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) && + NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) && + NumericGuard.IsFinite(point.ArcLength) && point.ArcLength >= 0d && + NumericGuard.IsFinite(point.GeometricCurvature) && NumericGuard.IsFinite(point.VehicleCurvature) && + NumericGuard.IsFinite(point.VehicleCurvatureDerivative) && + IsDirection(point.Direction) && Enum.IsDefined(typeof(SmoothedPathPointSource), point.Source) && + Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= Tolerance; + } + + private static bool SamePose(SmoothedPathPoint candidate, SmoothingPoint2D original) + { + return candidate != null && original != null && Math.Abs(candidate.X - original.X) <= Tolerance && + Math.Abs(candidate.Y - original.Y) <= Tolerance && + Math.Abs(AngleMath.ShortestSignedDifference(candidate.Heading, original.Heading)) <= Tolerance; + } + + private static bool IsUnwrappedHeadingContinuous(SmoothedPathPoint previous, SmoothedPathPoint current) + { + double expectedDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + return NumericGuard.IsFinite(expectedDelta) && + Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedDelta) <= Tolerance; + } + + private static bool IsDuplicatePoseAndArcLength(SmoothedPathPoint previous, SmoothedPathPoint current) + { + return Math.Abs(previous.X - current.X) <= Tolerance && Math.Abs(previous.Y - current.Y) <= Tolerance && + Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= Tolerance && + Math.Abs(previous.ArcLength - current.ArcLength) <= Tolerance; + } + + private static bool IsDirection(TravelDirection direction) + { + return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; + } + + private static IReadOnlyList EmptyPath() + { + return new ReadOnlyCollection(new List()); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs new file mode 100644 index 0000000..01271f2 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs @@ -0,0 +1,26 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 路径平滑对比报告使用的固定 IEEE 风格物理尺寸和色卡。 +public static class IeeeFigureStyle +{ + /// 双栏宽度,单位 pt。 + public const double FigureWidthPoints = 7.16d * 72d; + + /// 图形高度,单位 pt。 + public const double FigureHeightPoints = 5.20d * 72d; + + /// 原始粗路径颜色。 + public const string RawColor = "#4D4D4D"; + + /// 三次 B 样条颜色。 + public const string BSplineColor = "#0072B2"; + + /// 局部三次 Bézier 颜色。 + public const string BezierColor = "#D55E00"; + + /// 分段五次路径颜色。 + public const string QuinticColor = "#009E73"; + + /// 曲率限制颜色。 + public const string LimitColor = "#CC79A7"; +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs new file mode 100644 index 0000000..c324a35 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingCsvWriter.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 把共享图形模型的指标表写为带 BOM 的 UTF-8 CSV。 +public sealed class SmoothingCsvWriter +{ + private const string Header = "ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic,RetryCount,AcceptedStrength"; + + public byte[] Write(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + var text = new StringBuilder(Header).Append("\r\n"); + for (int index = 0; index < model.MetricRows.Count; index++) + { + SmoothingFigureMetricRow row = model.MetricRows[index]; + PathQualityMetrics metrics = row.Metrics; + text.Append(Field(model.ScenarioId)).Append(',').Append(Field(row.Method)).Append(',').Append(Field(row.Status.ToString())).Append(',') + .Append(Number(metrics.PathLengthMeters)).Append(',').Append(Number(metrics.MaximumAbsoluteVehicleCurvaturePerMeter)).Append(',') + .Append(Number(metrics.RootMeanSquareVehicleCurvaturePerMeter)).Append(',').Append(Number(metrics.TotalAbsoluteCurvatureVariationPerMeter)).Append(',') + .Append(Number(metrics.CurvatureVariationEnergy)).Append(',').Append(Number(metrics.MinimumBodyClearanceMeters)).Append(',') + .Append(Number(row.Timing == null ? 0d : row.Timing.MedianElapsedMilliseconds)).Append(',') + .Append(row.Timing == null ? 0 : row.Timing.MeasuredElapsedMilliseconds.Count).Append(',') + .Append(row.Timing != null && row.Timing.IsDeterministic ? "true" : "false").Append(',').Append(row.RetryCount).Append(',') + .Append(Number(row.AcceptedStrength)).Append("\r\n"); + } + byte[] body = new UTF8Encoding(false).GetBytes(text.ToString()); + byte[] preamble = new UTF8Encoding(true).GetPreamble(); + var output = new byte[preamble.Length + body.Length]; + Buffer.BlockCopy(preamble, 0, output, 0, preamble.Length); + Buffer.BlockCopy(body, 0, output, preamble.Length, body.Length); + return output; + } + + private static string Number(double value) { return value.ToString("0.#################", CultureInfo.InvariantCulture); } + private static string Field(string value) + { + string text = value ?? string.Empty; + return text.IndexOfAny(new[] { ',', '\"', '\r', '\n' }) < 0 ? text : "\"" + text.Replace("\"", "\"\"") + "\""; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs new file mode 100644 index 0000000..61dfb11 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 单张 SVG/PNG 共享的不可变绘图视图。 +public sealed class SmoothingFigureDefinition +{ + internal SmoothingFigureDefinition( + SmoothingFigureKind kind, + string fileStem, + string title, + SmoothingFigureModel model, + bool showsMapContext, + IReadOnlyList series, + double worldXMinMeters, + double worldXMaxMeters, + double worldYMinMeters, + double worldYMaxMeters, + IReadOnlyList xTicks, + IReadOnlyList yTicks, + double curvatureArcLengthMaximumMeters, + double curvatureMinimumPerMeter, + double curvatureMaximumPerMeter, + IReadOnlyList curvatureArcLengthTicks, + IReadOnlyList curvatureTicks) + { + Kind = kind; + FileStem = fileStem ?? string.Empty; + Title = title ?? string.Empty; + Model = model ?? throw new ArgumentNullException(nameof(model)); + ShowsMapContext = showsMapContext; + Series = Copy(series); + WorldXMinMeters = worldXMinMeters; + WorldXMaxMeters = worldXMaxMeters; + WorldYMinMeters = worldYMinMeters; + WorldYMaxMeters = worldYMaxMeters; + XTicks = Copy(xTicks); + YTicks = Copy(yTicks); + CurvatureArcLengthMaximumMeters = curvatureArcLengthMaximumMeters; + CurvatureMinimumPerMeter = curvatureMinimumPerMeter; + CurvatureMaximumPerMeter = curvatureMaximumPerMeter; + CurvatureArcLengthTicks = Copy(curvatureArcLengthTicks); + CurvatureTicks = Copy(curvatureTicks); + } + + public SmoothingFigureKind Kind { get; } + public string FileStem { get; } + public string Title { get; } + public SmoothingFigureModel Model { get; } + public bool ShowsMapContext { get; } + public bool IsCurvatureFigure => Kind == SmoothingFigureKind.CurvatureComparison; + public double FigureWidthPoints => Model.FigureWidthPoints; + public double FigureHeightPoints => Model.FigureHeightPoints; + public double PlotXPoints => 68d; + public double PlotYPoints => 44d; + public double PlotWidthPoints => 400d; + public double PlotHeightPoints => 245d; + public double LegendYPoints => 340d; + public IReadOnlyList Series { get; } + public IReadOnlyList LegendEntries => BuildLegend(Series); + public double WorldXMinMeters { get; } + public double WorldXMaxMeters { get; } + public double WorldYMinMeters { get; } + public double WorldYMaxMeters { get; } + public double WorldScalePointsPerMeter => Math.Min(PlotWidthPoints / (WorldXMaxMeters - WorldXMinMeters), PlotHeightPoints / (WorldYMaxMeters - WorldYMinMeters)); + public IReadOnlyList XTicks { get; } + public IReadOnlyList YTicks { get; } + public double CurvatureArcLengthMaximumMeters { get; } + public double CurvatureMinimumPerMeter { get; } + public double CurvatureMaximumPerMeter { get; } + public IReadOnlyList CurvatureArcLengthTicks { get; } + public IReadOnlyList CurvatureTicks { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList BuildLegend(IReadOnlyList views) + { + var entries = new List(views == null ? 0 : views.Count); + if (views != null) + { + for (int index = 0; index < views.Count; index++) + { + SmoothingFigureSeries series = views[index].Series; + entries.Add(new SmoothingFigureLegendEntry(series.Label + " (" + series.Status + ")", series.Color, string.Empty)); + } + } + return new ReadOnlyCollection(entries); + } +} + +/// 某条路径在特定图中的显示透明度。 +public sealed class SmoothingFigureSeriesView +{ + internal SmoothingFigureSeriesView(SmoothingFigureSeries series, double opacity) + { + Series = series ?? throw new ArgumentNullException(nameof(series)); + Opacity = opacity < 0d ? 0d : (opacity > 1d ? 1d : opacity); + } + + public SmoothingFigureSeries Series { get; } + public double Opacity { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs new file mode 100644 index 0000000..e55203b --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs @@ -0,0 +1,12 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 每个场景稳定发布的六种报告图。 +public enum SmoothingFigureKind +{ + CoarsePathOverview, + AllPathsComparison, + CubicBSplineOverview, + LocalCubicBezierOverview, + PiecewiseQuinticOverview, + CurvatureComparison, +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs new file mode 100644 index 0000000..22ebd8d --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// SVG 和 PNG 共享的不可变路径平滑报告图形模型。 +public sealed class SmoothingFigureModel +{ + internal SmoothingFigureModel( + string scenarioId, + string scenarioLabel, + double worldXMinMeters, + double worldXMaxMeters, + double worldYMinMeters, + double worldYMaxMeters, + double pathPanelX, + double pathPanelY, + double pathPanelWidth, + double pathPanelHeight, + double curvaturePanelX, + double curvaturePanelY, + double curvaturePanelWidth, + double curvaturePanelHeight, + double metricsPanelX, + double metricsPanelY, + double metricsPanelWidth, + double metricsPanelHeight, + IReadOnlyList obstacles, + IReadOnlyList series, + IReadOnlyList metricRows, + SmoothingFigurePoint start, + SmoothingFigurePoint goal) + { + ScenarioId = scenarioId ?? string.Empty; + ScenarioLabel = scenarioLabel ?? string.Empty; + WorldXMinMeters = worldXMinMeters; + WorldXMaxMeters = worldXMaxMeters; + WorldYMinMeters = worldYMinMeters; + WorldYMaxMeters = worldYMaxMeters; + PathPanelX = pathPanelX; + PathPanelY = pathPanelY; + PathPanelWidth = pathPanelWidth; + PathPanelHeight = pathPanelHeight; + CurvaturePanelX = curvaturePanelX; + CurvaturePanelY = curvaturePanelY; + CurvaturePanelWidth = curvaturePanelWidth; + CurvaturePanelHeight = curvaturePanelHeight; + MetricsPanelX = metricsPanelX; + MetricsPanelY = metricsPanelY; + MetricsPanelWidth = metricsPanelWidth; + MetricsPanelHeight = metricsPanelHeight; + Obstacles = Copy(obstacles); + Series = Copy(series); + MetricRows = Copy(metricRows); + Start = start ?? throw new ArgumentNullException(nameof(start)); + Goal = goal ?? throw new ArgumentNullException(nameof(goal)); + } + + public string ScenarioId { get; } + public string ScenarioLabel { get; } + public double FigureWidthPoints => IeeeFigureStyle.FigureWidthPoints; + public double FigureHeightPoints => IeeeFigureStyle.FigureHeightPoints; + public string PathPanelLabel => "(a)"; + public string CurvaturePanelLabel => "(b)"; + public string MetricsPanelLabel => "(c)"; + public double WorldXMinMeters { get; } + public double WorldXMaxMeters { get; } + public double WorldYMinMeters { get; } + public double WorldYMaxMeters { get; } + public double PathPanelX { get; } + public double PathPanelY { get; } + public double PathPanelWidth { get; } + public double PathPanelHeight { get; } + public double CurvaturePanelX { get; } + public double CurvaturePanelY { get; } + public double CurvaturePanelWidth { get; } + public double CurvaturePanelHeight { get; } + public double MetricsPanelX { get; } + public double MetricsPanelY { get; } + public double MetricsPanelWidth { get; } + public double MetricsPanelHeight { get; } + public double PathScaleX { get; internal set; } + public double PathScaleY { get; internal set; } + public IReadOnlyList Obstacles { get; } + public IReadOnlyList Series { get; } + public IReadOnlyList LegendEntries => BuildLegend(Series); + public IReadOnlyList MetricRows { get; } + public SmoothingFigurePoint Start { get; } + public SmoothingFigurePoint Goal { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList BuildLegend(IReadOnlyList series) + { + var legend = new List(series == null ? 0 : series.Count); + if (series != null) + { + for (int index = 0; index < series.Count; index++) + legend.Add(new SmoothingFigureLegendEntry(series[index].Label, series[index].Color, series[index].DashArray)); + } + return new ReadOnlyCollection(legend); + } +} + +public sealed class SmoothingFigurePoint +{ + public SmoothingFigurePoint(double xMeters, double yMeters, double arcLengthMeters, double vehicleCurvaturePerMeter) + { + X = xMeters; Y = yMeters; ArcLength = arcLengthMeters; VehicleCurvature = vehicleCurvaturePerMeter; + } + public double X { get; } + public double Y { get; } + public double ArcLength { get; } + public double VehicleCurvature { get; } +} + +public sealed class SmoothingFigureObstacle +{ + public SmoothingFigureObstacle(double xMeters, double yMeters, double widthMeters, double heightMeters) + { + X = xMeters; Y = yMeters; Width = widthMeters; Height = heightMeters; + } + public double X { get; } + public double Y { get; } + public double Width { get; } + public double Height { get; } +} + +public sealed class SmoothingFigureSeries +{ + internal SmoothingFigureSeries(SmoothingMethod? method, string key, string label, PathSmoothingStatus status, string color, string dashArray, + bool isRawPathBaseline, IReadOnlyList points, IReadOnlyList violationMarkers) + { + Method = method; Key = key ?? string.Empty; Label = label ?? string.Empty; Status = status; Color = color ?? string.Empty; + DashArray = dashArray ?? string.Empty; IsRawPathBaseline = isRawPathBaseline; Points = Copy(points); ViolationMarkers = Copy(violationMarkers); + } + public SmoothingMethod? Method { get; } + public string Key { get; } + public string Label { get; } + public PathSmoothingStatus Status { get; } + public string Color { get; } + public string DashArray { get; } + public bool IsRawPathBaseline { get; } + public bool IsCurveVisible => Points.Count > 0; + public IReadOnlyList Points { get; } + public IReadOnlyList ViolationMarkers { get; } + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} + +public sealed class SmoothingFigureLegendEntry +{ + internal SmoothingFigureLegendEntry(string label, string color, string dashArray) { Label = label ?? string.Empty; Color = color ?? string.Empty; DashArray = dashArray ?? string.Empty; } + public string Label { get; } + public string Color { get; } + public string DashArray { get; } +} + +public sealed class SmoothingFigureMetricRow +{ + internal SmoothingFigureMetricRow(string method, string label, PathSmoothingStatus status, PathQualityMetrics metrics, + SmoothingTimingSummary timing, int retryCount, double acceptedStrength) + { + Method = method ?? string.Empty; Label = label ?? string.Empty; Status = status; Metrics = metrics ?? new PathQualityMetrics(); + Timing = timing; RetryCount = retryCount < 0 ? 0 : retryCount; AcceptedStrength = acceptedStrength; + } + public string Method { get; } + public string Label { get; } + public PathSmoothingStatus Status { get; } + public PathQualityMetrics Metrics { get; } + public SmoothingTimingSummary Timing { get; } + public int RetryCount { get; } + public double AcceptedStrength { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs new file mode 100644 index 0000000..cbd48b3 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 将不可变的比较结果、地图和端点转换为共享报告图形模型。 +public sealed class SmoothingFigureModelBuilder +{ + private const double MarginPoints = 18d; + private const double PanelGapPoints = 12d; + + public SmoothingFigureModel Build( + PathSmoothingComparisonResult comparison, + PlanningGridMap map, + Pose2D start, + Pose2D goal, + string scenarioId, + string scenarioLabel) + { + if (comparison == null) throw new ArgumentNullException(nameof(comparison)); + if (map == null) throw new ArgumentNullException(nameof(map)); + if (start == null) throw new ArgumentNullException(nameof(start)); + if (goal == null) throw new ArgumentNullException(nameof(goal)); + + double worldXMin = map.Bounds.XMin / 1000d; + double worldXMax = map.Bounds.XMax / 1000d; + double worldYMin = map.Bounds.YMin / 1000d; + double worldYMax = map.Bounds.YMax / 1000d; + double innerWidth = IeeeFigureStyle.FigureWidthPoints - 2d * MarginPoints; + double innerHeight = IeeeFigureStyle.FigureHeightPoints - 2d * MarginPoints; + double pathWidth = innerWidth * 0.60d; + double rightWidth = innerWidth - pathWidth - PanelGapPoints; + double rightHeight = (innerHeight - PanelGapPoints) / 2d; + + var model = new SmoothingFigureModel( + scenarioId, scenarioLabel, worldXMin, worldXMax, worldYMin, worldYMax, + MarginPoints, MarginPoints, pathWidth, innerHeight, + MarginPoints + pathWidth + PanelGapPoints, MarginPoints, rightWidth, rightHeight, + MarginPoints + pathWidth + PanelGapPoints, MarginPoints + rightHeight + PanelGapPoints, rightWidth, rightHeight, + BuildObstacles(map), BuildSeries(comparison, map), BuildMetricRows(comparison), + new SmoothingFigurePoint(start.X, start.Y, 0d, 0d), new SmoothingFigurePoint(goal.X, goal.Y, 0d, 0d)); + + double worldWidth = worldXMax - worldXMin; + double worldHeight = worldYMax - worldYMin; + if (worldWidth <= 0d || worldHeight <= 0d) throw new ArgumentOutOfRangeException(nameof(map), "地图边界必须为非退化有限范围。"); + double scale = Math.Min(pathWidth / worldWidth, innerHeight / worldHeight); + model.PathScaleX = scale; + model.PathScaleY = scale; + return model; + } + + private static IReadOnlyList BuildObstacles(PlanningGridMap map) + { + var runs = new List(); + double resolution = map.ResolutionMeters; + double xMin = map.Bounds.XMin / 1000d; + double yMin = map.Bounds.YMin / 1000d; + for (int row = 0; row < map.Rows; row++) + { + int runStart = -1; + for (int column = 0; column <= map.Cols; column++) + { + bool occupied = column < map.Cols && map.IsOccupied(row, column); + if (occupied && runStart < 0) { runStart = column; continue; } + if (!occupied && runStart >= 0) + { + AddOrExtendRun(runs, xMin + runStart * resolution, yMin + row * resolution, + (column - runStart) * resolution, resolution); + runStart = -1; + } + } + } + var obstacles = new List(runs.Count); + for (int index = 0; index < runs.Count; index++) + { + ObstacleRun run = runs[index]; + obstacles.Add(new SmoothingFigureObstacle(run.X, run.Y, run.Width, run.Height)); + } + return obstacles; + } + + private static void AddOrExtendRun(IList runs, double x, double y, double width, double height) + { + for (int index = runs.Count - 1; index >= 0; index--) + { + ObstacleRun candidate = runs[index]; + if (NearlyEqual(candidate.X, x) && NearlyEqual(candidate.Width, width) && NearlyEqual(candidate.Y + candidate.Height, y)) + { + candidate.Height += height; + return; + } + } + runs.Add(new ObstacleRun(x, y, width, height)); + } + + private static bool NearlyEqual(double first, double second) { return Math.Abs(first - second) < 1e-9d; } + + private static IReadOnlyList BuildSeries(PathSmoothingComparisonResult comparison, PlanningGridMap map) + { + var series = new List(4) + { + CreateSeries(comparison.RawPathBaseline, null, "raw", "原始粗路径", IeeeFigureStyle.RawColor, string.Empty, true, map) + }; + series.Add(CreateSeries(Find(comparison, SmoothingMethod.CubicBSpline), SmoothingMethod.CubicBSpline, "bspline", "三次 B 样条", IeeeFigureStyle.BSplineColor, string.Empty, false, map)); + series.Add(CreateSeries(Find(comparison, SmoothingMethod.LocalCubicBezier), SmoothingMethod.LocalCubicBezier, "bezier", "局部三次 Bézier", IeeeFigureStyle.BezierColor, string.Empty, false, map)); + series.Add(CreateSeries(Find(comparison, SmoothingMethod.PiecewiseQuintic), SmoothingMethod.PiecewiseQuintic, "quintic", "分段五次", IeeeFigureStyle.QuinticColor, string.Empty, false, map)); + return series; + } + + private static IReadOnlyList BuildMetricRows(PathSmoothingComparisonResult comparison) + { + return new List + { + CreateRow(comparison.RawPathBaseline, "RawPath", "原始粗路径"), + CreateRow(Find(comparison, SmoothingMethod.CubicBSpline), "CubicBSpline", "三次 B 样条"), + CreateRow(Find(comparison, SmoothingMethod.LocalCubicBezier), "LocalCubicBezier", "局部三次 Bézier"), + CreateRow(Find(comparison, SmoothingMethod.PiecewiseQuintic), "PiecewiseQuintic", "分段五次") + }; + } + + private static PathSmoothingComparisonEntry Find(PathSmoothingComparisonResult comparison, SmoothingMethod method) + { + for (int index = 0; index < comparison.Entries.Count; index++) + { + PathSmoothingComparisonEntry entry = comparison.Entries[index]; + if (entry.Method == method) return entry; + } + return null; + } + + private static SmoothingFigureMetricRow CreateRow(PathSmoothingComparisonEntry entry, string method, string label) + { + return entry == null + ? new SmoothingFigureMetricRow(method, label, PathSmoothingStatus.Failed, new PathQualityMetrics(), null, 0, 0d) + : new SmoothingFigureMetricRow(method, label, entry.Status, entry.Metrics, entry.Timing, entry.RetryCount, entry.AcceptedStrength); + } + + private static SmoothingFigureSeries CreateSeries(PathSmoothingComparisonEntry entry, SmoothingMethod? method, string key, string label, + string color, string dashArray, bool isRawPathBaseline, PlanningGridMap map) + { + var points = new List(); + var violations = new List(); + PathSmoothingStatus status = entry == null ? PathSmoothingStatus.Failed : entry.Status; + if (entry != null) + { + for (int index = 0; index < entry.Path.Count; index++) + { + SmoothedPathPoint point = entry.Path[index]; + var figurePoint = new SmoothingFigurePoint(point.X, point.Y, point.ArcLength, point.VehicleCurvature); + points.Add(figurePoint); + if (status == PathSmoothingStatus.Infeasible && map.IsOccupiedWorld(point.X, point.Y)) violations.Add(figurePoint); + } + } + if (status == PathSmoothingStatus.Infeasible && points.Count > 0 && violations.Count == 0) + violations.Add(points[points.Count / 2]); + return new SmoothingFigureSeries(method, key, label, status, color, dashArray, isRawPathBaseline, points, violations); + } + + private sealed class ObstacleRun + { + public ObstacleRun(double x, double y, double width, double height) { X = x; Y = y; Width = width; Height = height; } + public double X { get; } + public double Y { get; } + public double Width { get; } + public double Height { get; set; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs new file mode 100644 index 0000000..00d3e82 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 从一份比较模型构建六张含义固定、相机聚焦的报告图。 +public sealed class SmoothingFigureSetBuilder +{ + private const double MinimumExtentMeters = 0.25d; + private const double PaddingFraction = 0.10d; + + public SmoothingFigureSet Build(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + SmoothingFigureSeries raw = Find(model, "raw"); + SmoothingFigureSeries bSpline = Find(model, "bspline"); + SmoothingFigureSeries bezier = Find(model, "bezier"); + SmoothingFigureSeries quintic = Find(model, "quintic"); + var figures = new List + { + BuildOverhead(SmoothingFigureKind.CoarsePathOverview, "01-coarse-path-overview", "Hybrid A* 原始粗路径", model, true, View(raw, 1d)), + BuildOverhead(SmoothingFigureKind.AllPathsComparison, "02-all-paths-comparison", "路径平滑结果对比", model, false, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)), + BuildOverhead(SmoothingFigureKind.CubicBSplineOverview, "03-cubic-bspline-overview", "三次 B 样条平滑", model, true, View(raw, 0.28d), View(bSpline, 1d)), + BuildOverhead(SmoothingFigureKind.LocalCubicBezierOverview, "04-local-cubic-bezier-overview", "局部三次 Bézier 平滑", model, true, View(raw, 0.28d), View(bezier, 1d)), + BuildOverhead(SmoothingFigureKind.PiecewiseQuinticOverview, "05-piecewise-quintic-overview", "分段五次平滑", model, true, View(raw, 0.28d), View(quintic, 1d)), + BuildCurvature(model, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)), + }; + return new SmoothingFigureSet(figures); + } + + private static SmoothingFigureDefinition BuildOverhead(SmoothingFigureKind kind, string stem, string title, SmoothingFigureModel model, bool mapContext, params SmoothingFigureSeriesView[] series) + { + Bounds bounds = CalculateWorldBounds(model, mapContext, series); + return new SmoothingFigureDefinition(kind, stem, title, model, mapContext, series, + bounds.XMin, bounds.XMax, bounds.YMin, bounds.YMax, + BuildTicks(bounds.XMin, bounds.XMax), BuildTicks(bounds.YMin, bounds.YMax), + 1d, -1d, 1d, Array.Empty(), Array.Empty()); + } + + private static SmoothingFigureDefinition BuildCurvature(SmoothingFigureModel model, params SmoothingFigureSeriesView[] series) + { + double arcMaximum = 0d; + double curvatureMinimum = 0d; + double curvatureMaximum = 0d; + for (int viewIndex = 0; viewIndex < series.Length; viewIndex++) + { + IReadOnlyList points = series[viewIndex].Series.Points; + for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) + { + SmoothingFigurePoint point = points[pointIndex]; + if (point.ArcLength > arcMaximum) arcMaximum = point.ArcLength; + if (point.VehicleCurvature < curvatureMinimum) curvatureMinimum = point.VehicleCurvature; + if (point.VehicleCurvature > curvatureMaximum) curvatureMaximum = point.VehicleCurvature; + } + } + arcMaximum = ExpandMaximum(arcMaximum, MinimumExtentMeters); + ExpandRange(ref curvatureMinimum, ref curvatureMaximum, MinimumExtentMeters); + return new SmoothingFigureDefinition(SmoothingFigureKind.CurvatureComparison, "06-curvature-comparison", "车辆曲率对比", model, false, series, + 0d, 1d, 0d, 1d, Array.Empty(), Array.Empty(), + arcMaximum, curvatureMinimum, curvatureMaximum, BuildTicks(0d, arcMaximum), BuildTicks(curvatureMinimum, curvatureMaximum)); + } + + private static Bounds CalculateWorldBounds(SmoothingFigureModel model, bool includesEndpoints, IReadOnlyList series) + { + bool hasPoint = false; + double xMin = 0d, xMax = 0d, yMin = 0d, yMax = 0d; + for (int viewIndex = 0; viewIndex < series.Count; viewIndex++) + { + IReadOnlyList points = series[viewIndex].Series.Points; + for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) Include(points[pointIndex].X, points[pointIndex].Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + } + if (includesEndpoints) + { + Include(model.Start.X, model.Start.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + Include(model.Goal.X, model.Goal.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + } + if (!hasPoint) + { + xMin = model.WorldXMinMeters; xMax = model.WorldXMaxMeters; + yMin = model.WorldYMinMeters; yMax = model.WorldYMaxMeters; + } + ExpandRange(ref xMin, ref xMax, MinimumExtentMeters); + ExpandRange(ref yMin, ref yMax, MinimumExtentMeters); + double xPadding = (xMax - xMin) * PaddingFraction; + double yPadding = (yMax - yMin) * PaddingFraction; + xMin -= xPadding; xMax += xPadding; yMin -= yPadding; yMax += yPadding; + double desiredAspect = 400d / 245d; + double width = xMax - xMin; + double height = yMax - yMin; + if (width / height < desiredAspect) + { + double halfWidth = height * desiredAspect / 2d; + double center = (xMin + xMax) / 2d; + xMin = center - halfWidth; xMax = center + halfWidth; + } + else + { + double halfHeight = width / desiredAspect / 2d; + double center = (yMin + yMax) / 2d; + yMin = center - halfHeight; yMax = center + halfHeight; + } + return new Bounds(xMin, xMax, yMin, yMax); + } + + private static void Include(double x, double y, ref bool hasPoint, ref double xMin, ref double xMax, ref double yMin, ref double yMax) + { + if (!hasPoint) { hasPoint = true; xMin = xMax = x; yMin = yMax = y; return; } + if (x < xMin) xMin = x; + if (x > xMax) xMax = x; + if (y < yMin) yMin = y; + if (y > yMax) yMax = y; + } + + private static void ExpandRange(ref double minimum, ref double maximum, double minimumExtent) + { + double extent = maximum - minimum; + if (extent >= minimumExtent) return; + double center = (minimum + maximum) / 2d; + minimum = center - minimumExtent / 2d; + maximum = center + minimumExtent / 2d; + } + + private static double ExpandMaximum(double value, double minimum) { return value < minimum ? minimum : value * (1d + PaddingFraction); } + + private static IReadOnlyList BuildTicks(double minimum, double maximum) + { + double span = maximum - minimum; + if (span <= 0d) return new[] { minimum, maximum }; + double roughStep = span / 5d; + double magnitude = Math.Pow(10d, Math.Floor(Math.Log10(roughStep))); + double normalized = roughStep / magnitude; + double nice = normalized <= 1d ? 1d : (normalized <= 2d ? 2d : (normalized <= 5d ? 5d : 10d)); + double step = nice * magnitude; + var ticks = new List(); + double first = Math.Ceiling(minimum / step) * step; + for (double value = first; value <= maximum + step * 0.001d; value += step) ticks.Add(value); + if (ticks.Count < 2) { ticks.Clear(); ticks.Add(minimum); ticks.Add(maximum); } + return new ReadOnlyCollection(ticks); + } + + private static SmoothingFigureSeries Find(SmoothingFigureModel model, string key) + { + for (int index = 0; index < model.Series.Count; index++) if (model.Series[index].Key == key) return model.Series[index]; + throw new InvalidOperationException("比较图形模型缺少路径序列: " + key); + } + + private static SmoothingFigureSeriesView View(SmoothingFigureSeries series, double opacity) { return new SmoothingFigureSeriesView(series, opacity); } + + private readonly struct Bounds + { + public Bounds(double xMin, double xMax, double yMin, double yMax) { XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; } + public double XMin { get; } + public double XMax { get; } + public double YMin { get; } + public double YMax { get; } + } +} + +/// 六张图的稳定顺序。 +public sealed class SmoothingFigureSet +{ + internal SmoothingFigureSet(IReadOnlyList figures) + { + var copy = new List(figures == null ? 0 : figures.Count); + if (figures != null) for (int index = 0; index < figures.Count; index++) copy.Add(figures[index]); + Figures = new ReadOnlyCollection(copy); + } + + public IReadOnlyList Figures { get; } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs new file mode 100644 index 0000000..61299d7 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFontResolver.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 按准确族名解析报告所需字体,并提供混合中英文文本的共同基线度量。 +public sealed class SmoothingFontResolver +{ + public bool TryResolve( + string chineseFamilyName, + string latinFamilyName, + out SmoothingFontResolution resolution, + out string reason) + { + resolution = null; + reason = string.Empty; + if (string.IsNullOrWhiteSpace(chineseFamilyName) || string.IsNullOrWhiteSpace(latinFamilyName)) + { + reason = "中文和拉丁字体族名均不能为空。"; + return false; + } + + var collection = new InstalledFontCollection(); + FontFamily chinese = FindExact(collection.Families, chineseFamilyName); + FontFamily latin = FindExact(collection.Families, latinFamilyName); + if (chinese == null || latin == null) + { + collection.Dispose(); + reason = "未安装报告所需的精确字体族:" + (chinese == null ? chineseFamilyName : latinFamilyName) + "。"; + return false; + } + + resolution = new SmoothingFontResolution(collection, chinese, latin, chineseFamilyName, latinFamilyName); + return true; + } + + public RectangleF MeasureMixedText(SmoothingFontResolution resolution, string text, float points) + { + if (resolution == null) throw new ArgumentNullException(nameof(resolution)); + if (string.IsNullOrEmpty(text) || points <= 0f) return RectangleF.Empty; + using (var bitmap = new Bitmap(1, 1)) + using (Graphics graphics = Graphics.FromImage(bitmap)) + using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) + { + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + float width = 0f, height = 0f; + foreach (TextRun run in SplitRuns(text)) + { + using (Font font = resolution.CreateFont(run.IsChinese ? resolution.ChineseFamily : resolution.LatinFamily, points)) + { + SizeF size = graphics.MeasureString(run.Text, font, PointF.Empty, format); + width += size.Width; + height = Math.Max(height, size.Height); + } + } + return new RectangleF(0f, 0f, width, height); + } + } + + internal static IReadOnlyList SplitRuns(string text) + { + var runs = new List(); + if (string.IsNullOrEmpty(text)) return runs; + int start = 0; + bool isChinese = IsChinese(text[0]); + for (int index = 1; index < text.Length; index++) + { + bool currentIsChinese = IsChinese(text[index]); + if (currentIsChinese == isChinese) continue; + runs.Add(new TextRun(text.Substring(start, index - start), isChinese)); + start = index; + isChinese = currentIsChinese; + } + runs.Add(new TextRun(text.Substring(start), isChinese)); + return runs; + } + + private static FontFamily FindExact(IReadOnlyList families, string name) + { + for (int index = 0; index < families.Count; index++) + { + FontFamily family = families[index]; + if (string.Equals(family.Name, name, StringComparison.Ordinal) || + string.Equals(family.GetName(1033), name, StringComparison.Ordinal)) return family; + } + return null; + } + + private static bool IsChinese(char value) + { + return (value >= 0x3400 && value <= 0x4dbf) || (value >= 0x4e00 && value <= 0x9fff) || + (value >= 0xf900 && value <= 0xfaff) || value == 0x3002 || value == 0xff0c || value == 0xff1a; + } + + internal sealed class TextRun + { + public TextRun(string text, bool isChinese) { Text = text; IsChinese = isChinese; } + public string Text { get; } + public bool IsChinese { get; } + } +} + +/// 一次报告导出所使用的精确字体族;释放时同时释放字体集合。 +public sealed class SmoothingFontResolution : IDisposable +{ + private readonly InstalledFontCollection _collection; + private readonly string _chineseFamilyName; + private readonly string _latinFamilyName; + + internal SmoothingFontResolution( + InstalledFontCollection collection, + FontFamily chineseFamily, + FontFamily latinFamily, + string chineseFamilyName, + string latinFamilyName) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + ChineseFamily = chineseFamily ?? throw new ArgumentNullException(nameof(chineseFamily)); + LatinFamily = latinFamily ?? throw new ArgumentNullException(nameof(latinFamily)); + _chineseFamilyName = chineseFamilyName ?? throw new ArgumentNullException(nameof(chineseFamilyName)); + _latinFamilyName = latinFamilyName ?? throw new ArgumentNullException(nameof(latinFamilyName)); + } + + public string ChineseFamilyName => _chineseFamilyName; + public string LatinFamilyName => _latinFamilyName; + internal FontFamily ChineseFamily { get; } + internal FontFamily LatinFamily { get; } + + internal Font CreateFont(FontFamily family, float points) + { + return new Font(family, points, FontStyle.Regular, GraphicsUnit.Point); + } + + public void Dispose() + { + _collection.Dispose(); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs new file mode 100644 index 0000000..24ac0b8 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs @@ -0,0 +1,287 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 仅 Windows 运行时使用 GDI+ 将每张共享图形定义渲染为 600 dpi PNG;轨迹仅绘制离散点。 +public sealed class SmoothingPngRenderer +{ + public const int WidthPixels = 4296; + public const int HeightPixels = 3120; + public const uint PixelsPerMeter = 23622u; + private const float PixelsPerPoint = 600f / 72f; + + public byte[] Render(SmoothingFigureModel model, SmoothingFontResolution fonts) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1], fonts); + } + + public byte[] Render(SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + if (fonts == null) throw new ArgumentNullException(nameof(fonts)); + using (var bitmap = new Bitmap(WidthPixels, HeightPixels, PixelFormat.Format32bppArgb)) + { + bitmap.SetResolution(600f, 600f); + using (Graphics graphics = Graphics.FromImage(bitmap)) + { + graphics.Clear(Color.White); + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + DrawFigure(graphics, figure, fonts); + } + return Encode(bitmap); + } + } + + private static void DrawFigure(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawMixedText(graphics, fonts, figure.Title + " — " + figure.Model.ScenarioLabel, PointX(figure.PlotXPoints), PointY(12d), 12f, Color.Black); + if (figure.IsCurvatureFigure) DrawCurvatureAxes(graphics, figure, fonts); else DrawOverheadAxes(graphics, figure, fonts); + GraphicsState state = graphics.Save(); + graphics.SetClip(new RectangleF(PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints))); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawObstacles(graphics, figure); + if (figure.IsCurvatureFigure) DrawCurvaturePoints(graphics, figure); else DrawPathPoints(graphics, figure); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawStartGoal(graphics, figure); + graphics.Restore(state); + DrawLegend(graphics, figure, fonts); + } + + private static void DrawOverheadAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawPlotFrame(graphics, figure); + using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) + { + for (int index = 0; index < figure.XTicks.Count; index++) + { + float x = WorldX(figure, figure.XTicks[index]); + graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); + DrawMixedText(graphics, fonts, Number(figure.XTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); + } + for (int index = 0; index < figure.YTicks.Count; index++) + { + float y = WorldY(figure, figure.YTicks[index]); + graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + DrawMixedText(graphics, fonts, Number(figure.YTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); + } + } + DrawMixedText(graphics, fonts, "X (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 11d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); + DrawVerticalText(graphics, fonts, "Y (m)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 17d), 10f, Color.Black); + } + + private static void DrawCurvatureAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawPlotFrame(graphics, figure); + using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) + { + for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) + { + float x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); + graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); + DrawMixedText(graphics, fonts, Number(figure.CurvatureArcLengthTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); + } + for (int index = 0; index < figure.CurvatureTicks.Count; index++) + { + float y = CurvatureY(figure, figure.CurvatureTicks[index]); + graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + DrawMixedText(graphics, fonts, Number(figure.CurvatureTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); + } + } + if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) + { + float y = CurvatureY(figure, 0d); + using (var zero = new Pen(Color.FromArgb(77, 77, 77), 0.65f * PixelsPerPoint)) + graphics.DrawLine(zero, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + } + DrawMixedText(graphics, fonts, "s (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 10d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); + DrawVerticalText(graphics, fonts, "κ (m⁻¹)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 22d), 10f, Color.Black); + } + + private static void DrawPlotFrame(Graphics graphics, SmoothingFigureDefinition figure) + { + using (var border = new Pen(Color.Black, 0.75f * PixelsPerPoint)) + graphics.DrawRectangle(border, PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints)); + } + + private static void DrawObstacles(Graphics graphics, SmoothingFigureDefinition figure) + { + using (var fill = new SolidBrush(Color.FromArgb(217, 217, 217))) + using (var outline = new Pen(Color.FromArgb(128, 128, 128), 0.35f * PixelsPerPoint)) + { + for (int index = 0; index < figure.Model.Obstacles.Count; index++) + { + SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; + float x = WorldX(figure, obstacle.X); + float y = WorldY(figure, obstacle.Y + obstacle.Height); + float width = (float)(obstacle.Width * figure.WorldScalePointsPerMeter * PixelsPerPoint); + float height = (float)(obstacle.Height * figure.WorldScalePointsPerMeter * PixelsPerPoint); + graphics.FillRectangle(fill, x, y, width, height); + graphics.DrawRectangle(outline, x, y, width, height); + } + } + } + + private static void DrawPathPoints(Graphics graphics, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + DrawPoints(graphics, view, point => new PointF(WorldX(figure, point.X), WorldY(figure, point.Y))); + DrawViolations(graphics, figure, view); + } + } + + private static void DrawCurvaturePoints(Graphics graphics, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + DrawPoints(graphics, view, point => new PointF(CurvatureX(figure, point.ArcLength), CurvatureY(figure, point.VehicleCurvature))); + } + } + + private static void DrawPoints(Graphics graphics, SmoothingFigureSeriesView view, Func transform) + { + if (!view.Series.IsCurveVisible) return; + using (var fill = new SolidBrush(WithOpacity(ColorFromHex(view.Series.Color), view.Opacity))) + { + float radius = 1.35f * PixelsPerPoint; + for (int index = 0; index < view.Series.Points.Count; index++) + { + PointF point = transform(view.Series.Points[index]); + graphics.FillEllipse(fill, point.X - radius, point.Y - radius, radius * 2f, radius * 2f); + } + } + } + + private static void DrawViolations(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFigureSeriesView view) + { + if (view.Series.ViolationMarkers.Count == 0) return; + using (var marker = new Pen(ColorFromHex(view.Series.Color), 1.1f * PixelsPerPoint)) + { + float radius = 3f * PixelsPerPoint; + for (int index = 0; index < view.Series.ViolationMarkers.Count; index++) + { + SmoothingFigurePoint point = view.Series.ViolationMarkers[index]; + float x = WorldX(figure, point.X), y = WorldY(figure, point.Y); + graphics.DrawLine(marker, x - radius, y - radius, x + radius, y + radius); + graphics.DrawLine(marker, x - radius, y + radius, x + radius, y - radius); + } + } + } + + private static void DrawStartGoal(Graphics graphics, SmoothingFigureDefinition figure) + { + float startX = WorldX(figure, figure.Model.Start.X), startY = WorldY(figure, figure.Model.Start.Y), radius = 3.2f * PixelsPerPoint; + using (var startBrush = new SolidBrush(Color.FromArgb(240, 228, 66))) + using (var border = new Pen(Color.Black, 0.8f * PixelsPerPoint)) + using (var goalBrush = new SolidBrush(ColorFromHex(IeeeFigureStyle.LimitColor))) + { + graphics.FillEllipse(startBrush, startX - radius, startY - radius, radius * 2f, radius * 2f); + graphics.DrawEllipse(border, startX - radius, startY - radius, radius * 2f, radius * 2f); + float goalX = WorldX(figure, figure.Model.Goal.X), goalY = WorldY(figure, figure.Model.Goal.Y), diamond = 4f * PixelsPerPoint; + PointF[] points = { new PointF(goalX, goalY - diamond), new PointF(goalX + diamond, goalY), new PointF(goalX, goalY + diamond), new PointF(goalX - diamond, goalY) }; + graphics.FillPolygon(goalBrush, points); + graphics.DrawPolygon(border, points); + } + } + + private static void DrawLegend(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + const double columnWidth = 198d; + for (int index = 0; index < figure.LegendEntries.Count; index++) + { + SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; + int column = index % 2, row = index / 2; + float x = PointX(figure.PlotXPoints + column * columnWidth); + float y = PointY(figure.LegendYPoints + row * 15d - 3d); + using (var fill = new SolidBrush(ColorFromHex(entry.Color))) graphics.FillEllipse(fill, x, y - 2.2f * PixelsPerPoint, 4.4f * PixelsPerPoint, 4.4f * PixelsPerPoint); + DrawMixedText(graphics, fonts, entry.Label, x + 10f * PixelsPerPoint, y - 5f * PixelsPerPoint, 8.5f, Color.Black); + } + } + + private static void DrawVerticalText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float y, float points, Color color) + { + GraphicsState state = graphics.Save(); + graphics.TranslateTransform(x, y); + graphics.RotateTransform(-90f); + DrawMixedText(graphics, fonts, text, 0f, 0f, points, color); + graphics.Restore(state); + } + + private static void DrawMixedText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float top, float points, Color color) + { + var runs = SmoothingFontResolver.SplitRuns(text); + float emPixels = points * PixelsPerPoint, maxAscent = 0f; + for (int index = 0; index < runs.Count; index++) + { + FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; + maxAscent = Math.Max(maxAscent, family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular)); + } + float baseline = top + maxAscent; + using (var brush = new SolidBrush(color)) + using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) + { + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + for (int index = 0; index < runs.Count; index++) + { + FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; + using (Font font = fonts.CreateFont(family, points)) + { + float runTop = baseline - family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular); + graphics.DrawString(runs[index].Text, font, brush, x, runTop, format); + x += graphics.MeasureString(runs[index].Text, font, PointF.Empty, format).Width; + } + } + } + } + + private static byte[] Encode(Bitmap bitmap) + { + Rectangle rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + BitmapData data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + int sourceLength = checked(data.Stride * bitmap.Height); + var source = new byte[sourceLength]; + Marshal.Copy(data.Scan0, source, 0, source.Length); + var rgba = new byte[checked(bitmap.Width * bitmap.Height * 4)]; + for (int y = 0; y < bitmap.Height; y++) + { + int sourceRow = y * data.Stride, outputRow = y * bitmap.Width * 4; + for (int x = 0; x < bitmap.Width; x++) + { + int sourceOffset = sourceRow + x * 4, outputOffset = outputRow + x * 4; + rgba[outputOffset] = source[sourceOffset + 2]; + rgba[outputOffset + 1] = source[sourceOffset + 1]; + rgba[outputOffset + 2] = source[sourceOffset]; + rgba[outputOffset + 3] = source[sourceOffset + 3]; + } + } + using (var output = new MemoryStream()) + { + ValidatedPngWriter.Write(rgba, bitmap.Width, bitmap.Height, output, PixelsPerMeter); + return output.ToArray(); + } + } + finally { bitmap.UnlockBits(data); } + } + + private static Color ColorFromHex(string value) { return ColorTranslator.FromHtml(value); } + private static Color WithOpacity(Color color, double opacity) { return Color.FromArgb((int)Math.Round(255d * opacity), color.R, color.G, color.B); } + private static float PointX(double points) { return (float)(points * PixelsPerPoint); } + private static float PointY(double points) { return (float)(points * PixelsPerPoint); } + private static float WorldX(SmoothingFigureDefinition figure, double x) { return PointX(figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter); } + private static float WorldY(SmoothingFigureDefinition figure, double y) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter); } + private static float CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return PointX(figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints); } + private static float CurvatureY(SmoothingFigureDefinition figure, double curvature) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints); } + private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs new file mode 100644 index 0000000..31aa234 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportRequest.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 一次报告导出的共享图形模型、目标目录与精确字体要求。 +public sealed class SmoothingReportExportRequest +{ + public SmoothingFigureModel Model { get; set; } + public string OutputDirectory { get; set; } + public string FileStem { get; set; } + public string ChineseFontFamilyName { get; set; } = "SimSun"; + public string LatinFontFamilyName { get; set; } = "Times New Roman"; +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs new file mode 100644 index 0000000..c4dda77 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 报告导出结果的稳定状态。 +public enum SmoothingReportExportStatus +{ + Success, + InvalidInput, + FontUnavailable, + Failed, +} + +/// 六张 SVG、六张 PNG 与一个 CSV 的发布结果;失败时不返回部分输出路径。 +public sealed class SmoothingReportExportResult +{ + internal SmoothingReportExportResult(SmoothingReportExportStatus status, string reason, IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) + { + Status = status; + Reason = reason ?? string.Empty; + SvgPaths = Copy(svgPaths); + PngPaths = Copy(pngPaths); + CsvPath = csvPath ?? string.Empty; + } + + public SmoothingReportExportStatus Status { get; } + public string Reason { get; } + public IReadOnlyList SvgPaths { get; } + public IReadOnlyList PngPaths { get; } + public string CsvPath { get; } + + internal static SmoothingReportExportResult Success(IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) + { + return new SmoothingReportExportResult(SmoothingReportExportStatus.Success, string.Empty, svgPaths, pngPaths, csvPath); + } + + internal static SmoothingReportExportResult Failure(SmoothingReportExportStatus status, string reason) + { + return new SmoothingReportExportResult(status, reason, new string[0], new string[0], string.Empty); + } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index] ?? string.Empty); + return new ReadOnlyCollection(copy); + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs new file mode 100644 index 0000000..55d6de0 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 以同级临时文件生成六张 SVG、六张 PNG 和 CSV 后成组发布。 +public sealed class SmoothingReportExporter +{ + private readonly SmoothingFontResolver _fontResolver = new SmoothingFontResolver(); + private readonly SmoothingFigureSetBuilder _figureSetBuilder = new SmoothingFigureSetBuilder(); + private readonly SmoothingSvgRenderer _svgRenderer = new SmoothingSvgRenderer(); + private readonly SmoothingPngRenderer _pngRenderer = new SmoothingPngRenderer(); + private readonly SmoothingCsvWriter _csvWriter = new SmoothingCsvWriter(); + + public SmoothingReportExportResult Export(SmoothingReportExportRequest request) + { + if (request == null || request.Model == null || string.IsNullOrWhiteSpace(request.OutputDirectory) || !IsFileStem(request.FileStem)) + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.InvalidInput, "报告模型、输出目录或文件名无效。"); + if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution availableFonts, out string fontReason)) + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.FontUnavailable, fontReason); + availableFonts.Dispose(); + + var pending = new List(); + var svgPaths = new List(); + var pngPaths = new List(); + string csvPath = Path.Combine(request.OutputDirectory, request.FileStem + ".csv"); + try + { + SmoothingFigureSet figures = _figureSetBuilder.Build(request.Model); + for (int index = 0; index < figures.Figures.Count; index++) + { + SmoothingFigureDefinition figure = figures.Figures[index]; + string svgPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".svg"); + string pngPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".png"); + svgPaths.Add(svgPath); + pngPaths.Add(pngPath); + pending.Add(new PendingFile(svgPath, Encoding.UTF8.GetBytes(_svgRenderer.Render(figure)))); + if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution renderFonts, out fontReason)) + throw new InvalidOperationException(fontReason); + byte[] png; + using (renderFonts) png = _pngRenderer.Render(figure, renderFonts); + pending.Add(new PendingFile(pngPath, png)); + } + pending.Add(new PendingFile(csvPath, _csvWriter.Write(request.Model))); + Directory.CreateDirectory(request.OutputDirectory); + for (int index = 0; index < pending.Count; index++) File.WriteAllBytes(pending[index].TemporaryPath, pending[index].Content); + PublishAll(pending); + DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.svg")); + DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.png")); + return SmoothingReportExportResult.Success(svgPaths, pngPaths, csvPath); + } + catch (Exception exception) + { + RestorePublishedFiles(pending); + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.Failed, exception.Message); + } + finally + { + for (int index = 0; index < pending.Count; index++) + { + DeleteIfExists(pending[index].TemporaryPath); + DeleteIfExists(pending[index].BackupPath); + } + } + } + + private static void PublishAll(IReadOnlyList files) + { + string transaction = Guid.NewGuid().ToString("N"); + for (int index = 0; index < files.Count; index++) + { + PendingFile file = files[index]; + file.ExistedBeforePublish = File.Exists(file.FinalPath); + file.BackupPath = file.ExistedBeforePublish ? file.FinalPath + ".backup-" + transaction : string.Empty; + if (file.ExistedBeforePublish) File.Replace(file.TemporaryPath, file.FinalPath, file.BackupPath); + else File.Move(file.TemporaryPath, file.FinalPath); + file.Published = true; + } + for (int index = 0; index < files.Count; index++) DeleteIfExists(files[index].BackupPath); + } + + private static void RestorePublishedFiles(IReadOnlyList files) + { + for (int index = files.Count - 1; index >= 0; index--) + { + PendingFile file = files[index]; + if (!file.Published) continue; + try + { + if (file.ExistedBeforePublish && File.Exists(file.BackupPath)) + { + if (File.Exists(file.FinalPath)) File.Replace(file.BackupPath, file.FinalPath, null); + else File.Move(file.BackupPath, file.FinalPath); + } + else if (!file.ExistedBeforePublish) DeleteIfExists(file.FinalPath); + } + catch { } + } + } + + private static bool IsFileStem(string value) + { + return !string.IsNullOrWhiteSpace(value) && value.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 && value.IndexOf(Path.DirectorySeparatorChar) < 0 && value.IndexOf(Path.AltDirectorySeparatorChar) < 0; + } + + private static void DeleteIfExists(string path) + { + if (!string.IsNullOrEmpty(path) && File.Exists(path)) File.Delete(path); + } + + private sealed class PendingFile + { + public PendingFile(string finalPath, byte[] content) + { + FinalPath = finalPath; + Content = content; + TemporaryPath = finalPath + ".tmp"; + BackupPath = string.Empty; + } + + public string FinalPath { get; } + public byte[] Content { get; } + public string TemporaryPath { get; } + public string BackupPath { get; set; } + public bool ExistedBeforePublish { get; set; } + public bool Published { get; set; } + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs new file mode 100644 index 0000000..5cbb46e --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs @@ -0,0 +1,189 @@ +using System; +using System.Globalization; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +/// 把单张共享图形定义渲染为 UTF-8 XML 可编辑 SVG;轨迹仅由离散点组成。 +public sealed class SmoothingSvgRenderer +{ + public string Render(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1]); + } + + public string Render(SmoothingFigureDefinition figure) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + var svg = new StringBuilder(); + svg.Append("\n\n") + .Append("\n") + .Append("\n"); + AppendTitle(svg, figure); + if (figure.IsCurvatureFigure) AppendCurvatureAxes(svg, figure); else AppendOverheadAxes(svg, figure); + svg.Append("\n"); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendObstacles(svg, figure); + if (figure.IsCurvatureFigure) AppendCurvaturePoints(svg, figure); else AppendPathPoints(svg, figure); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendStartGoal(svg, figure); + svg.Append("\n"); + AppendLegend(svg, figure); + svg.Append(""); + return svg.ToString(); + } + + private static void AppendTitle(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("") + .Append(Escape(figure.Title)).Append(" — ").Append(Escape(figure.Model.ScenarioLabel)).Append("\n"); + } + + private static void AppendOverheadAxes(StringBuilder svg, SmoothingFigureDefinition figure) + { + AppendPlotFrame(svg, figure); + for (int index = 0; index < figure.XTicks.Count; index++) + { + double x = WorldX(figure, figure.XTicks[index]); + AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); + svg.Append("").Append(Number(figure.XTicks[index])).Append("\n"); + } + for (int index = 0; index < figure.YTicks.Count; index++) + { + double y = WorldY(figure, figure.YTicks[index]); + AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); + svg.Append("").Append(Number(figure.YTicks[index])).Append("\n"); + } + svg.Append("X (m)\n") + .Append("Y (m)\n"); + } + + private static void AppendCurvatureAxes(StringBuilder svg, SmoothingFigureDefinition figure) + { + AppendPlotFrame(svg, figure); + for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) + { + double x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); + AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); + svg.Append("").Append(Number(figure.CurvatureArcLengthTicks[index])).Append("\n"); + } + for (int index = 0; index < figure.CurvatureTicks.Count; index++) + { + double y = CurvatureY(figure, figure.CurvatureTicks[index]); + AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); + svg.Append("").Append(Number(figure.CurvatureTicks[index])).Append("\n"); + } + if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) + { + double zero = CurvatureY(figure, 0d); + svg.Append("\n"); + } + svg.Append("s (m)\n") + .Append("κ (m⁻¹)\n"); + } + + private static void AppendPlotFrame(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("\n"); + } + + private static void AppendGridLine(StringBuilder svg, double x1, double y1, double x2, double y2) + { + svg.Append("\n"); + } + + private static void AppendObstacles(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int index = 0; index < figure.Model.Obstacles.Count; index++) + { + SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; + svg.Append("\n"); + } + } + + private static void AppendPathPoints(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + AppendPoints(svg, view, point => WorldX(figure, point.X), point => WorldY(figure, point.Y)); + for (int markerIndex = 0; markerIndex < view.Series.ViolationMarkers.Count; markerIndex++) AppendCross(svg, figure, view.Series.ViolationMarkers[markerIndex], view.Series.Color); + } + } + + private static void AppendCurvaturePoints(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + AppendPoints(svg, view, point => CurvatureX(figure, point.ArcLength), point => CurvatureY(figure, point.VehicleCurvature)); + } + } + + private static void AppendPoints(StringBuilder svg, SmoothingFigureSeriesView view, Func x, Func y) + { + if (!view.Series.IsCurveVisible) return; + svg.Append("\n"); + for (int index = 0; index < view.Series.Points.Count; index++) + { + SmoothingFigurePoint point = view.Series.Points[index]; + svg.Append("\n"); + } + svg.Append("\n"); + } + + private static void AppendCross(StringBuilder svg, SmoothingFigureDefinition figure, SmoothingFigurePoint point, string color) + { + double x = WorldX(figure, point.X), y = WorldY(figure, point.Y), radius = 3d; + svg.Append("\n"); + } + + private static void AppendStartGoal(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("\n"); + double x = WorldX(figure, figure.Model.Goal.X), y = WorldY(figure, figure.Model.Goal.Y), radius = 4d; + svg.Append("\n"); + } + + private static void AppendLegend(StringBuilder svg, SmoothingFigureDefinition figure) + { + double columnWidth = 198d; + for (int index = 0; index < figure.LegendEntries.Count; index++) + { + SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; + int column = index % 2; + int row = index / 2; + double x = figure.PlotXPoints + column * columnWidth; + double y = figure.LegendYPoints + row * 15d; + svg.Append("\n") + .Append(Escape(entry.Label)).Append("\n"); + } + } + + private static double WorldX(SmoothingFigureDefinition figure, double x) { return figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter; } + private static double WorldY(SmoothingFigureDefinition figure, double y) { return figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter; } + private static double CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints; } + private static double CurvatureY(SmoothingFigureDefinition figure, double curvature) { return figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints; } + private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } + private static string Escape(string value) { return (value ?? string.Empty).Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs b/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs new file mode 100644 index 0000000..5490ddc --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/AngleMath.cs @@ -0,0 +1,34 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// Deterministic angle helpers used by planning code. +public static class AngleMath +{ + public static double NormalizeRadians(double radians) + { + if (!NumericGuard.IsFinite(radians)) + return double.NaN; + + double normalized = radians % (2d * Math.PI); + if (normalized >= Math.PI) normalized -= 2d * Math.PI; + if (normalized < -Math.PI) normalized += 2d * Math.PI; + return normalized; + } + + public static double ShortestSignedDifference(double from, double to) + { + return NormalizeRadians(to - from); + } + + public static int ToHeadingIndex(double heading, double resolution, int binCount) + { + if (!NumericGuard.IsFinite(heading) || !NumericGuard.IsPositiveFinite(resolution) || binCount <= 0) + throw new ArgumentOutOfRangeException(); + + double normalized = NormalizeRadians(heading); + int index = (int)Math.Floor((normalized + Math.PI) / resolution); + index %= binCount; + return index < 0 ? index + binCount : index; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs b/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs new file mode 100644 index 0000000..44d4cea --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/CoordinateTransform.cs @@ -0,0 +1,18 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// Two-dimensional rigid-body coordinate transforms. Units are preserved. +public static class CoordinateTransform +{ + public static void LocalToWorld( + double originX, double originY, double headingRadians, + double localX, double localY, + out double worldX, out double worldY) + { + double cosine = Math.Cos(headingRadians); + double sine = Math.Sin(headingRadians); + worldX = originX + cosine * localX - sine * localY; + worldY = originY + sine * localX + cosine * localY; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs b/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs new file mode 100644 index 0000000..5683184 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/GridIndex.cs @@ -0,0 +1,15 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public readonly struct GridIndex : IEquatable +{ + public GridIndex(int row, int col) { Row = row; Col = col; } + public int Row { get; } + public int Col { get; } + public bool Equals(GridIndex other) { return Row == other.Row && Col == other.Col; } + public override bool Equals(object obj) { return obj is GridIndex && Equals((GridIndex)obj); } + public override int GetHashCode() { unchecked { return Row * 397 ^ Col; } } + public static bool operator ==(GridIndex left, GridIndex right) { return left.Equals(right); } + public static bool operator !=(GridIndex left, GridIndex right) { return !left.Equals(right); } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs b/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs new file mode 100644 index 0000000..0714ad4 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/NumericGuard.cs @@ -0,0 +1,14 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public static class NumericGuard +{ + public static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } + public static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } + public static bool IsPositiveFinite(double value) { return IsFinite(value) && value > 0d; } + public static bool IsInRange(float value, float minimum, float maximum) + { + return IsFinite(value) && value >= minimum && value <= maximum; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs b/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs new file mode 100644 index 0000000..2217a45 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// 一次规划操作的中立停止原因;Map 与 CoarsePath 分别映射为自己的结果状态。 +internal enum PlanningOperationStopReason +{ + /// 预算仍可继续执行。 + None, + /// 调用方取消了本次操作。 + Cancelled, + /// 从预算创建起已经达到总超时。 + TimedOut, +} + +/// +/// 以单调时钟统一控制一次规划操作的取消和总超时。 +/// 本类不依赖 Map 或 CoarsePath,供两层在长循环检查同一个停止条件。 +/// +internal sealed class PlanningOperationBudget +{ + private const int CheckIntervalWorkItems = 256; + + private readonly CancellationToken _cancellationToken; + private readonly Stopwatch _stopwatch; + private readonly TimeSpan _timeout; + private readonly bool _hasTimeout; + + /// + /// 创建具有总超时的操作预算。 + /// 参数:cancellationToken 为调用方停止请求;timeout 必须非负,且从构造时开始计时。 + /// + internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout) + { + if (timeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout)); + _cancellationToken = cancellationToken; + _timeout = timeout; + _hasTimeout = true; + _stopwatch = Stopwatch.StartNew(); + } + + private PlanningOperationBudget(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + _timeout = TimeSpan.Zero; + _hasTimeout = false; + _stopwatch = Stopwatch.StartNew(); + } + + /// 创建只响应取消、不限制总耗时的兼容预算。 + internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken) + { + return new PlanningOperationBudget(cancellationToken); + } + + /// 从预算创建到当前的单调经过时间。 + internal TimeSpan Elapsed { get { return _stopwatch.Elapsed; } } + + /// 立即查询当前停止原因;取消优先于超时。 + internal PlanningOperationStopReason GetStopReason() + { + if (_cancellationToken.IsCancellationRequested) return PlanningOperationStopReason.Cancelled; + return _hasTimeout && _stopwatch.Elapsed >= _timeout + ? PlanningOperationStopReason.TimedOut + : PlanningOperationStopReason.None; + } + + /// + /// 在循环工作单元中按固定上限检查预算。 + /// 参数:workItemCount 为当前循环共享的已处理单元计数;首次及每 256 个单元检查一次。 + /// + internal PlanningOperationStopReason CheckEvery(ref int workItemCount) + { + workItemCount++; + return workItemCount == 1 || workItemCount % CheckIntervalWorkItems == 0 + ? GetStopReason() + : PlanningOperationStopReason.None; + } +} diff --git a/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs b/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs new file mode 100644 index 0000000..7c41b97 --- /dev/null +++ b/.task8-sweep/ParkrobTrajplanner/Utils/UnitConverter.cs @@ -0,0 +1,11 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public static class UnitConverter +{ + public static double MillimetersToMeters(double value) { return value / 1000d; } + public static double MetersToMillimeters(double value) { return value * 1000d; } + public static double DegreesToRadians(double value) { return value * Math.PI / 180d; } + public static double RadiansToDegrees(double value) { return value * 180d / Math.PI; } +} diff --git a/.task8-sweep/PilotConfig.cs b/.task8-sweep/PilotConfig.cs new file mode 100644 index 0000000..57dab71 --- /dev/null +++ b/.task8-sweep/PilotConfig.cs @@ -0,0 +1,334 @@ +using ClumsyCore; +using MDCSToolBox.Clumsy.Pilot.MultiWheel; +using Newtonsoft.Json; + +namespace MultiWheelC; + +public class PilotConfig : MultiWheelPilotConfig +{ + [FieldMember(desc = "[sync] steering angle acceleration(deg/s^2)")] public float SyncThAccPerSec = 30f; + [FieldMember(desc = "[sync] fleet member distance(mm)")] public float TestCarSyncDistance = 2400f; + [FieldMember(desc = "[sync] fleet layout bias angle(deg)")] public float TestCarSyncTh = 0f; + // Fleet manual remote IO values are normalized joystick ratios. Keep all speed/angle scaling here. + [FieldMember(desc = "[sync] fleet manual max linear speed(m/s)")] public float FleetManualMaxSpeed = 0.3f; + [FieldMember(desc = "[sync] fleet manual normal-mode full-stick steering angle(deg)")] public float FleetManualMaxSteerAngleDeg = 45f; + [FieldMember(desc = "[sync] fleet manual crab-mode full-stick steering angle(deg)")] public float FleetManualMaxCrabAngleDeg = 60f; + [FieldMember(desc = "[sync] fleet manual rotate-mode full-stick angular speed(deg/s)")] public float FleetManualMaxRotateOmegaDegPerSec = 45f; + [FieldMember(desc = "[sync] 蟹行舵角上限(deg,应与Medulla舵轮角度限制匹配,默认120)")] public float MultiVehicleCrabSteerLimitDeg = 120f; + [FieldMember(desc = "[sync] 检测中心偏移(mm)")] public float DeltaDetectCenter = 350f; + // 仅控制"车队内姿态纠正"(POS 补偿)是否使用 Detour 的 SLAM 位姿,不影响"整个车队姿态的计算"。 + // 默认 false:定位不参与车队内姿态纠正(各车按编队几何/互识别保持队形,不做 SLAM 逐车纠偏)。 + // 为 true:额外用 getCartLocation() 反推每台车相对编队中心的偏差并做 POS 补偿。 + // 注意:无论该开关如何,自动模式下整队姿态(反推/广播车队中心、SLAM 间距、自动安全门)始终依赖 Detour 全局定位; + // 主车自动模式必调用 getCartLocation(),若无有效全局定位该调用会阻塞 → 联动线程阻塞不下发速度(安全停车)。 + [FieldMember(desc = "[sync] 定位是否参与车队内姿态纠正(不影响整队姿态计算)")] public bool MultiVehicleSyncUseDetour = false; + // 手动外部遥控联动默认只走 2 腿检测/几何同步,避免 Detour getCartLocation 阻塞导致遥控和检测可视化变慢。 + [FieldMember(desc = "[sync] 手动联动是否启用定位姿态纠正(默认关闭)")] public bool MultiVehicleManualUseDetourCorrection = false; + + [FieldMember(desc = "多车联动:总车数")] public int MultiVehicleFleetNum = 2; + [FieldMember(desc = "联动线程周期(ms)")] public int MultiVehicleSyncInterval = 50; + [FieldMember(desc = "多车联动:主车端点 ip:port,/ 表示本车为主车")] public string MultiVehicleMasterEndpoint = "/"; + [FieldMember(desc = "多车联动:本车同步 IP")] public string SimpleIp = "127.0.0.1"; + + [FieldMember(desc = "多车联动:本车回连端点 ip:port,供主车 notify 回连,空=127.0.0.1:本车port")] public string MultiVehicleSelfEndpoint = ""; + + [JsonProperty("MultiVehicleMasterIp")] + private string LegacyMasterIpSetter + { + set + { + if (string.IsNullOrEmpty(value) || value == "/") return; + if (MultiVehicleMasterEndpoint == "/") + MultiVehicleMasterEndpoint = value.Contains(":") ? value : $"{value}:8008"; + } + } + + [FieldMember(desc = "多车联动:启用互识别纠正")] public bool MultiVehicleUseDetect = false; + + // B: 自动速度命令新鲜度(ms)。主车超过此时长未从路径控制器收到新速度命令(路径结束/早退/卡顿), + // 即视为失效并清零下发速度,避免车队按末速度滑行。0 表示自动取 max(200, interval*4)。 + [FieldMember(desc = "多车联动:自动速度命令超时(ms,0=auto)")] public int MultiVehicleAutoCmdTimeoutMs = 0; + // C: fleet 成员存活 TTL(ms)。主车剔除超过此时长未 register/刷新的从车;编队就绪要求所有成员新鲜。 + // 0 表示自动取 max(500, interval*6)。 + [FieldMember(desc = "多车联动:成员存活TTL(ms,0=auto)")] public int MultiVehicleMemberTtlMs = 0; + // D: 自动模式下用主车路径控制器的理想车队中心(idealPos/idealAngle)作为各车 layout 目标, + // 弧线路径上做 per-car 前馈而非仅共用 frontTh/rearTh 事后纠偏。 + [FieldMember(desc = "多车联动:自动模式按理想中心前馈(弧线)")] public bool MultiVehicleAutoUseIdealCenter = true; + // H: 自动模式必须有有效车队中心(SLAM 可反推),全程定位丢失时停车,避免纯 SLAM 下盲跑。 + [FieldMember(desc = "多车联动:自动模式要求有效车队中心")] public bool MultiVehicleAutoRequireFleetCenter = true; + [FieldMember(desc = "多车联动:SLAM X补偿系数")] public float MultiVehiclePosBiasXFac = 0.5f; + [FieldMember(desc = "多车联动:SLAM Y补偿系数")] public float MultiVehiclePosBiasYFac = 0.5f; + [FieldMember(desc = "多车联动:SLAM Th补偿系数")] public float MultiVehiclePosBiasThFac = 0.5f; + [FieldMember(desc = "多车联动:X补偿阈值(mm)")] public float MultiVehiclePosBiasXThreshold = 50f; + [FieldMember(desc = "多车联动:Y补偿阈值(mm)")] public float MultiVehiclePosBiasYThreshold = 50f; + [FieldMember(desc = "多车联动:Th补偿阈值(deg)")] public float MultiVehiclePosBiasThThreshold = 5f; + + [FieldMember(desc = "多车联动:互识别 X补偿系数")] public float MultiVehicleDetectBiasXFac = 0.5f; + [FieldMember(desc = "多车联动:互识别 Y补偿系数")] public float MultiVehicleDetectBiasYFac = 0.5f; + [FieldMember(desc = "多车联动:互识别 Th补偿系数")] public float MultiVehicleDetectBiasThFac = 0.5f; + [FieldMember(desc = "多车联动:互识别 X补偿阈值(mm)")] public float MultiVehicleDetectBiasXThreshold = 50f; + [FieldMember(desc = "多车联动:互识别 Y补偿阈值(mm)")] public float MultiVehicleDetectBiasYThreshold = 50f; + [FieldMember(desc = "多车联动:互识别 Th补偿阈值(deg)")] public float MultiVehicleDetectBiasThThreshold = 5f; + + // 原地旋转(mode2)闭环纠偏(PI):把"本车应移动到的位置(dx,dy,mm)/应转角(dth,deg)"作为误差, + // 用 PI 控制器换算成车体系修正速度叠加到绕队心旋转上。纯 P 对抗恒定横向滑移扰动有稳态残差, + // 加积分项把稳态误差拉到 0;积分带限幅(抗 windup),总输出限幅在 Max 内防过冲/振荡。 + // Fac=比例增益(mm/s per mm、deg/s per deg);IFac=积分增益(mm/s per mm·s、deg/s per deg·s);Max=总输出上限。 + [FieldMember(desc = "原地旋转纠偏:平移比例增益P(mm/s per mm)")] public float MultiVehicleRotateCompXyFac = 1.2f; + [FieldMember(desc = "原地旋转纠偏:平移积分增益I(mm/s per mm·s)")] public float MultiVehicleRotateCompXyIFac = 0.8f; + [FieldMember(desc = "原地旋转纠偏:平移速度上限(mm/s)")] public float MultiVehicleRotateCompXyMax = 150f; + [FieldMember(desc = "原地旋转纠偏:转向比例增益P(deg/s per deg)")] public float MultiVehicleRotateCompThFac = 0.8f; + [FieldMember(desc = "原地旋转纠偏:转向积分增益I(deg/s per deg·s)")] public float MultiVehicleRotateCompThIFac = 0.8f; + [FieldMember(desc = "原地旋转纠偏:转向速度上限(deg/s)")] public float MultiVehicleRotateCompThMax = 15f; + // 仅当车队实际被指令旋转(|fleetOmega|超过此阈值)时才运行纠偏 PI;否则清零并复位积分, + // 避免松开摇杆后积分残留持续驱动车辆"自行旋转停不下来"。 + [FieldMember(desc = "原地旋转纠偏:生效的最小角速度阈值(deg/s)")] public float MultiVehicleRotateActiveOmega = 0.5f; + // 安全网:每轮纠偏速度幅值 <= 该比例 * 本轮旋转切向速度,限制合速度相对纯切向的最大偏角。 + // 旧配置若仍为 <0,运行时按安全默认 0.10 处理;确需放宽时可在主车显式调大并同步给从车。 + [FieldMember(desc = "原地旋转纠偏:纠偏/旋转切向比例硬上限,<0使用安全默认0.10")] public float MultiVehicleRotateCompTangentFrac = 0.10f; + + [FieldMember(desc = "单车同步 xy 精度(mm)")] public float SingleCarSyncPrecisionXy = 10f; + [FieldMember(desc = "单车同步 th 精度(deg)")] public float SingleCarSyncPrecisionTh = 0.2f; + + [FieldMember(desc = "Playground WebAPI 基地址")] + public string PlaygroundWebApiUrl = "http://localhost:18090"; + + [FieldMember(desc = "MultiVehicle rotate pose WebAPI diagnostics (simulation only)")] + public bool MultiVehicleRotatePoseWebApiDiagEnabled = false; + + [FieldMember(desc = "Playground 小车名称(场景 robots[].name)")] + public string PlaygroundRobotName = "agv_multi_1"; + + [FieldMember(desc = "Playground 邻车名称(仅主车用于原地旋转位姿诊断)")] + public string PlaygroundNeighborRobotName = "agv_multi_2"; + + [FieldMember(desc = "WebAPI 平移测试:平移距离(mm)")] + public float WebApiTranslateMm = 100f; + + [FieldMember(desc = "WebAPI 旋转测试:旋转角度(deg)")] + public float WebApiRotateDeg = 5f; + + [FieldMember(desc = "原地旋转:目标朝向(世界坐标系, deg)")] + public float InPlaceRotateTargetWorldDeg = 90f; + + [FieldMember(desc = "原地旋转:旋转角速度(deg/s)")] + public float InPlaceRotateSpeed = 30f; + + [FieldMember(desc = "原地旋转:到位角度精度(deg)")] + public float InPlaceRotateArriveDeg = 1f; + + [FieldMember(desc = "原地旋转:起转前舵轮对齐精度(deg)")] + public float InPlaceRotateWheelAlignDeg = 2f; + + [FieldMember(desc = "原地旋转:旋转过程中舵轮偏差重对齐阈值(deg)")] + public float InPlaceRotateActiveWheelAlignDeg = 10f; + + // ===== 车队联动-原地旋转动作(FleetRotateInPlace / 对应 FleetRemote 原地旋转模式)===== + // 通过 Clumsy 内部脚本字段驱动 TickMultiVehicle 的 mode2 旋转(绕车队中心 + PI 纠偏),需主车运行。 + [FieldMember(desc = "车队原地旋转:角速度大小(deg/s,方向由目标角符号决定)")] + public float FleetRotateOmega = 15f; + + [FieldMember(desc = "车队原地旋转:目标相对转角(deg,+逆时针)")] + public float FleetRotateTargetDeltaDeg = 90f; + + [FieldMember(desc = "车队原地旋转:到位角度精度(deg)")] + public float FleetRotateArriveDeg = 1.5f; + + [FieldMember(desc = "车队原地旋转:减速区宽度(deg),抑制收尾惯性超调")] + public float FleetRotateSlowDeg = 25f; + + [FieldMember(desc = "车队原地旋转:减速区末段最小角速度(deg/s)")] + public float FleetRotateMinOmega = 3f; + + [FieldMember(desc = "车队原地旋转:起步缓启动角加速度(deg/s²,<=0关闭)")] + public float FleetRotateAccel = 20f; + + [FieldMember(desc = "车队原地旋转:到位后安定时长(s)")] + public float FleetRotateSettleSec = 0.5f; + + // 与 MultiVehicleSyncUseDetour 解耦:转到指定角度需航向反馈,默认 true 读主车 SLAM 航向闭环判停。 + // false 时退化为按估算时长开环停止(实际转速≠指令时不精确,易出现"没转到目标就停")。 + [FieldMember(desc = "车队原地旋转:用Detour主车航向闭环判停(默认true,false=按时长开环)")] + public bool FleetRotateUseDetourHeading = true; + + // ===== 车队联动-自动蟹行(FleetCrabWalk)===== + // 以当前车队中心为起点,构造一条直线路径;MovementTest 中车身保持启动朝向追踪该路径。 + // 动作侧参考几何控制器的路径跟踪思路,直接写入 MultiVehicleAuto... 字段,不再复用脚本手动链路。 + [FieldMember(desc = "车队蟹行:路径方向相对启动时车队朝向夹角(deg,逆时针为正;路径在车右侧x度时填-x)")] + public float FleetCrabAngleDeg = 45f; + + [FieldMember(desc = "车队蟹行:AGV入口使用的车队世界系目标朝向(deg)")] + public float FleetCrabBodyWorldHeadingDeg = 0f; + + [FieldMember(desc = "车队蟹行:路径长度(mm)")] + public float FleetCrabLengthMm = 2000f; + + [FieldMember(desc = "车队蟹行:行驶速度(m/s)")] + public float FleetCrabSpeed = 0.2f; + + [FieldMember(desc = "车队蟹行:速度命令加速度限制(m/s^2,<=0表示不限制)")] + public float FleetCrabAccel = 0.2f; + + [FieldMember(desc = "车队蟹行:预对齐后正式下发速度前5秒加速度(m/s^2,<=0表示不限制)")] + public float FleetCrabStartAccel = 0.01f; + + [FieldMember(desc = "车队蟹行:末端开始减速距离(mm)")] + public float FleetCrabSlowDistance = 2000f; + + [FieldMember(desc = "车队蟹行:完成距离(mm),低于该剩余距离结束动作")] + public float FleetCrabFinishDistance = 20f; + + [FieldMember(desc = "车队蟹行:末端最低速度(m/s)")] + public float FleetCrabFinishSpeed = 0.02f; + + [FieldMember(desc = "车队蟹行:末端减速曲线指数")] + public float FleetCrabSlowingPow = 0.8f; + + [FieldMember(desc = "车队蟹行:GCP舵角修正上限(deg)")] + public float FleetCrabGcpThetaThreshold = 95f; + + [FieldMember(desc = "车队蟹行:headingErr角度纠偏比例系数")] + public float FleetCrabDthLinearFac = 1f; + + [FieldMember(desc = "车队蟹行:headingErr角度纠偏舵角限幅(deg)")] + public float FleetCrabDthLinearThreshold = 10f; + + [FieldMember(desc = "FleetCrab startup sync timeout(s)")] + public float FleetCrabStartSyncTimeoutSec = 8f; + + [FieldMember(desc = "FleetCrab startup wheel alignment tolerance(deg)")] + public float FleetCrabStartWheelAlignDeg = 2f; + + // ===== Fleet linked Bezier curve walk ===== + [FieldMember(desc = "FleetCurve MovementTest Bezier control point count")] + public int FleetCurveTestControlPointCount = 4; + + [FieldMember(desc = "FleetCurve speed(m/s)")] + public float FleetCurveSpeed = 0.2f; + + [FieldMember(desc = "FleetCurve slow distance(mm)")] + public float FleetCurveSlowDistance = 2000f; + + [FieldMember(desc = "FleetCurve finish distance(mm)")] + public float FleetCurveFinishDistance = 20f; + + [FieldMember(desc = "FleetCurve finish speed(m/s)")] + public float FleetCurveFinishSpeed = 0.02f; + + [FieldMember(desc = "FleetCurve slowing curve exponent")] + public float FleetCurveSlowingPow = 0.8f; + + // ===== 2腿检测(单线雷达识别两腿托盘 / 轮胎)===== + [FieldMember(desc = "2腿检测:雷达名(逗号分隔可多个)")] + public string TwoLegLidarName = "rear_left_lidar_1,rear_right_lidar_1"; + + [FieldMember(desc = "2腿检测:初始猜测X(mm, 车体坐标系)")] + public float TwoLegGuessX = 2000f; + + [FieldMember(desc = "2腿检测:两腿间距(mm)")] + public float TwoLegWidth = 800f; + + [FieldMember(desc = "2腿检测:两腿间距允许误差(mm)")] + public float TwoLegWidthErr = 100f; + + [FieldMember(desc = "2腿检测:聚类点间距(mm)")] + public float TwoLegBlobDist = 100f; + + [FieldMember(desc = "2腿检测:聚类尺寸(mm)")] + public float TwoLegBlobSize = 200f; + + [FieldMember(desc = "2腿检测:聚类最小点数")] + public int TwoLegBlobPtCount = 5; + + [FieldMember(desc = "2腿检测:聚类 padding")] + public int TwoLegPadding = 5; + + [FieldMember(desc = "2腿检测:腿柱搜索范围")] + public int TwoLegPillarFindingScope = 20; + + [FieldMember(desc = "2腿检测:方向符号(±1)")] + public int TwoLegSgnDir = 1; + + [FieldMember(desc = "2腿检测:中心X偏移(mm)")] + public float TwoLegCenterChangeX = 0f; + + [FieldMember(desc = "2腿检测:输出X补偿(mm)")] + public float TwoLegOutputBiasX = 0f; + + [FieldMember(desc = "2腿检测:输出Y补偿(mm)")] + public float TwoLegOutputBiasY = 0f; + + [FieldMember(desc = "2腿检测:ROI滤波框长(mm)")] + public float TwoLegFilterLength = 1800f; + + [FieldMember(desc = "2腿检测:ROI滤波框宽(mm)")] + public float TwoLegFilterWidth = 600f; + + #region + [FieldMember(desc = "轮胎识别:识别框长")] public float TireFilterLength = 1800f; + [FieldMember(desc = "轮胎识别:识别框宽")] public float TireFilterWidth = 600f; + [FieldMember(desc = "轮胎识别:轮胎间距")] public float TireTwoLegWidth = 800f; + [FieldMember(desc = "轮胎识别:轮胎识别允许误差")] public float TireTwoLegWidthErr = 100f; + [FieldMember(desc = "轮胎识别:轮胎聚类最小点云数")] public int TireTwoLegBlobPtCount = 15; + + [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegBlobDist = 100f; + [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegBlobSize = 200f; + [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontPadding = 5; + [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontTwoLegPillarFindingScope = 20; + [FieldMember(desc = "轮胎识别:前雷达参数")] public int TireFrontTwoLegSgnDir = 1; + [FieldMember(desc = "轮胎识别:前雷达参数")] public float TireFrontTwoLegCenterChangeX = 0; + [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegBlobDist = 100f; + [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegBlobSize = 200f; + [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackPadding = 5; + [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackTwoLegPillarFindingScope = 20; + [FieldMember(desc = "轮胎识别:后雷达参数")] public int TireBackTwoLegSgnDir = 1; + [FieldMember(desc = "轮胎识别:后雷达参数")] public float TireBackTwoLegCenterChangeX = 0; + + [FieldMember(desc = "抱夹控制pid:Kp")] public float ClampControlKp = 0.1f; + [FieldMember(desc = "抱夹控制pid:Ki")] public float ClampControlKi = 0f; + [FieldMember(desc = "抱夹控制pid:Kd")] public float ClampControlKd = 0f; + [FieldMember(desc = "抱夹控制pid:MaxI")] public float ClampControlMaxI = 0f; + [FieldMember(desc = "抱夹控制pid:Acc")] public float ClampControlSpeedAcc = 1f; + [FieldMember(desc = "抱夹控制pid:Thresh")] public float ClampControlThresh = 0.2f; + [FieldMember(desc = "抱夹控制pid:DeadZone")] public float ClampControlDeadZone = 5f; + [FieldMember(desc = "抱夹最大速度")] public float MaxClampSpeed = 1.5f; + + [FieldMember(desc = "直线行走距离")] public float LineTrackDistance = 1000f; + [FieldMember(desc = "直线行走最大速度")] public float LineTrackMaxSpeed = 0.3f; + [FieldMember(desc = "直线行走Kp")] public float LineTrackKp = 0.2f; + [FieldMember(desc = "直线行走Ki")] public float LineTrackKi = 0f; + [FieldMember(desc = "直线行走Kd")] public float LineTrackKd = 0f; + [FieldMember(desc = "直线行走DeadZone")] public float LineTrackDeadZone = 50f; + + [FieldMember(desc = "轮胎跟踪:切换至盲走距离")] public float TireFollowingWalkBlindSwitchingDistance = 1200f; + [FieldMember(desc = "轮胎跟踪:识别第一对轮胎的初始距离")] public float TireFollowingStage1GuessX = 2000f; + [FieldMember(desc = "轮胎跟踪:识别第二对轮胎的初始距离")] public float TireFollowingStage2GuessX = 2475f; + [FieldMember(desc = "轮胎跟踪:盲走停止距离")] public float TireFollowingWalkBlindFinishDistance = 10f; + [FieldMember(desc = "轮胎跟踪:减速距离")] public float TireFollowingSlowDistance = 200f; + [FieldMember(desc = "轮胎跟踪:最大速度")] public float TireFollowingMaxSpeed = 0.2f; + [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移X")] public float TireFollowingFrontLidarPathTransformationX = 253f; + [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移Y")] public float TireFollowingFrontLidarPathTransformationY = 13f; + [FieldMember(desc = "轮胎跟踪:前雷达识别路径偏移Th")] public float TireFollowingFrontLidarWalkBlindTh = -1f; + [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移X")] public float TireFollowingBackLidarPathTransformationX = 148f; + [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移Y")] public float TireFollowingBackLidarPathTransformationY = 2f; + [FieldMember(desc = "轮胎跟踪:后雷达识别路径偏移Th")] public float TireFollowingBackLidarWalkBlindTh = 0f; + [FieldMember(desc = "轮胎跟踪:离车时后雷达识别路径偏移X")] public float TireFollowingLeaveCarBackLidarPathTransformationX = 1500f; + [FieldMember(desc = "轮胎跟踪:离车时切换至盲走距离")] public float TireFollowingLeaveCarWalkBlindSwitchingDistance = 1200f; + + [FieldMember(desc = "轮胎跟踪:测试钻轮胎数量")] public int TireFollowingTireNum = 1; + [FieldMember(desc = "轮胎跟踪:过近距离")] public float TireFollowingCloseDistance = 1400; + [FieldMember(desc = "轮胎跟踪:距离过近角度忽略阈值")] public float TireFollowingAngleIgnoreThr = 0.2f; + [FieldMember(desc = "轮胎跟踪:Y最大平均数")] public int TireFollowingYAverageFrameCount = 5; + + [FieldMember(desc = "终点跟踪:速度")] public float DstTrackerMaxSpeed = 0.3f; + [FieldMember(desc = "轮胎跟踪:释放锁点距离")] public float TireFollowingReleaseDistance = 1600; + #endregion + + [FieldMember(desc = "轮胎跟踪:角度调整kp")] public float TireFollowingThkp = 0.05f; + [FieldMember(desc = "轮胎跟踪:角度调整ki")] public float TireFollowingThki = 0.01f; + [FieldMember(desc = "轮胎跟踪:角度调整kd")] public float TireFollowingThkd = 0f; + [FieldMember(desc = "轮胎跟踪:角度调整SpeedAcc")] public float TireFollowingThSpeedAccPerSec = 1f; + [FieldMember(desc = "轮胎跟踪:角度调整Thresh")] public float TireFollowingThThresh = 0.1f; + [FieldMember(desc = "轮胎跟踪:角度调整DeadZone")] public float TireFollowingThDeadZone = 5f; + [FieldMember(desc = "轮胎跟踪:角度调整MaxI")] public float TireFollowingThMaxI = 0.01f; +} diff --git a/.task8-sweep/PilotDefinition.cs b/.task8-sweep/PilotDefinition.cs new file mode 100644 index 0000000..3352b6e --- /dev/null +++ b/.task8-sweep/PilotDefinition.cs @@ -0,0 +1,2128 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Net.Http; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using ClumsyCore.Utilities; +using CommonUsage; +using CommonUsage.Chassis; +using CommonUsage.Mathematics; +using FundamentalLib; +using FundamentalLib.Utilities; +using MDCSToolBox.Clumsy.Pilot.MultiWheel; +using Vector = ClumsyCore.Utilities.Vector; + +namespace MultiWheelC; + +public class PilotDefinition : MultiWheelPilotDefinition +{ + public new float CarLength = 1472f; + public new float CarWidth = 948f; + + public override void StandardInit() + { + InitMultiVehicleCoordination(); + } + + #region MultiVehicleCoordination (从 MDCSToolBox 内联回 Tutorial) + + [AsLowerIO(desc = "启用(手动)多车联动")] public bool MultiVehicleManualEnabled; + [AsLowerIO(desc = "(手动)多车联动模式")] public int MultiVehicleManualMode; + [FieldMember(desc = "多车联动已同步")] public bool MultiVehicleAligned; + + [AsLowerIO(desc = "多车联动:遥控器Vx比例")] public float MultiVehicleManualVx; + [AsLowerIO(desc = "多车联动:遥控器Vy比例")] public float MultiVehicleManualVy; + [AsLowerIO(desc = "多车联动:遥控器Vth比例")] public float MultiVehicleManualVth; + [AsLowerIO(desc = "多车联动:遥控暂停")] public bool MultiVehicleHold; + + // ===== Clumsy 侧脚本/动作驱动的手动等价输入(不走 Medulla IO,不会被 IO 同步覆盖)===== + // 手动 IO 字段是 [AsLowerIO](Medulla→Clumsy,Medulla 每周期回写),Clumsy 侧 MovementTest 写它们会被覆盖。 + // 因此提供这组内部字段,让 Clumsy 侧动作(如 FleetRotateInPlace)能像 FleetRemote 一样驱动车队联动。 + // ScriptEnabled=使能;Mode 0=常规 1=蟹行 2=原地旋转。 + // 注意:Medulla 遥控 IO 是归一化摇杆比例;脚本字段保持物理量,避免动作参数再被 FleetManual* 二次缩放。 + // Mode0: Vx=m/s, Vth=目标舵角deg;Mode1: Vx=m/s, Vy=蟹行舵角deg;Mode2: Vth=角速度deg/s。 + [FieldMember(desc = "多车联动:脚本驱动使能")] public bool MultiVehicleScriptEnabled; + [FieldMember(desc = "多车联动:脚本驱动模式")] public int MultiVehicleScriptMode; + [FieldMember(desc = "多车联动:脚本驱动Vx")] public float MultiVehicleScriptVx; + [FieldMember(desc = "多车联动:脚本驱动Vy")] public float MultiVehicleScriptVy; + [FieldMember(desc = "多车联动:脚本驱动Vth")] public float MultiVehicleScriptVth; + + [AsUpperIO(desc = "多车联动:自动驾驶", timeOutReset = true)] public bool MultiVehicleAutoEnabled; + [FieldMember(desc = "多车联动:自动驾驶Vx")] public float MultiVehicleAutoVx; + [FieldMember(desc = "多车联动:自动驾驶FrontTh")] public float MultiVehicleAutoFrontTh; + [FieldMember(desc = "多车联动:自动驾驶RearTh")] public float MultiVehicleAutoRearTh; + + public Dictionary MultiVehicleFleet = new(); + public VehicleSyncNotification MultiVehicleNotification; + + // A: 互斥锁固定为独立 readonly 对象,绝不随 MultiVehicleFleet 字段被整体替换而失效。 + // 所有对 MultiVehicleFleet / _multiVehicleFleetSeen 的读写都必须 lock(FleetLock)。 + public readonly object FleetLock = new(); + + // C: 各 fleet 成员最近一次被 register/notify 刷新的本地时刻(用本地时钟,避免远端时钟偏差)。 + // 主车 Tick 据此剔除掉线成员;编队就绪要求所有成员新鲜。 + private readonly Dictionary _multiVehicleFleetSeen = new(); + + [FieldMember(desc = "多车联动:车队姿态x")] public float CenterX; + [FieldMember(desc = "多车联动:车队姿态y")] public float CenterY; + [FieldMember(desc = "多车联动:车队姿态th")] public float CenterTh; + + // G: 车队中心原子快照。三个 float 无法整体原子写,改为整体新建对象后引用赋值(引用赋值原子), + // 路径控制器线程只读最近一次完整快照,避免读到 x 新 / y,th 旧的撕裂组合。 + public sealed class FleetCenterSnapshot + { + public float X, Y, Th; + public long Tick; + } + private volatile FleetCenterSnapshot _fleetCenter = new(); + public FleetCenterSnapshot GetFleetCenterSnapshot() => _fleetCenter; + private void PublishFleetCenter(float x, float y, float th) + { + CenterX = x; + CenterY = y; + CenterTh = th; + _fleetCenter = new FleetCenterSnapshot { X = x, Y = y, Th = th, Tick = DateTime.Now.Ticks }; + } + + // B: 路径控制器最近一次写入自动速度命令的时刻;超时即视为失效(路径结束/早退/卡顿),清零下发。 + public DateTime MultiVehicleAutoCmdTime = DateTime.MinValue; + // D: 路径控制器透传的理想车队中心位姿(世界系),由 idealPos/idealAngle 写入。 + public bool MultiVehicleAutoHasIdeal; + public float MultiVehicleAutoIdealX, MultiVehicleAutoIdealY, MultiVehicleAutoIdealTh; + + // F: notify 序列号(主车单调递增)与从车已应用的最大序列号(丢弃乱序旧包)。 + private long _multiVehicleNotifySeq; + private long _multiVehicleAppliedSeq = -1; + + [AsLowerIO(desc = "车号")] public int CarNum = 1; + [FieldMember(desc = "多车联动:原地旋转本车舵轮已对齐")] + public bool MultiVehicleRotateWheelsReady = true; + [FieldMember(desc = "多车联动:原地旋转整队舵轮已对齐")] + public bool MultiVehicleRotateFleetReady = true; + + #region 夹臂变量 + [AsUpperIO(desc = "左夹臂下发速度")] public float SpeedLeftArm; + + [AsUpperIO(desc = "右夹臂下发速度")] public float SpeedRightArm; + + [AsLowerIO(desc = "左夹臂实际位置")] public float ActualPosLeftArm; + + [AsLowerIO(desc = "右夹臂实际位置")] public float ActualPosRightArm; + + [AsUpperIO(desc = "夹臂不同步报警")] public bool ClampOutOfSync = false; + #endregion + + [AsLowerIO(desc = "左前左轮实际位置")] public float LFLActualPos; + [AsLowerIO(desc = "左前右轮实际位置")] public float LFRActualPos; + [AsLowerIO(desc = "右前左轮实际位置")] public float RFLActualPos; + [AsLowerIO(desc = "右前右轮实际位置")] public float RFRActualPos; + [AsLowerIO(desc = "左后左轮实际位置")] public float LRLActualPos; + [AsLowerIO(desc = "左后右轮实际位置")] public float LRRActualPos; + [AsLowerIO(desc = "右后左轮实际位置")] public float RRLActualPos; + [AsLowerIO(desc = "右后右轮实际位置")] public float RRRActualPos; + + [AsLowerIO(desc = "左夹臂低限位")] public float LeftArmLowerPos; + [AsLowerIO(desc = "左夹臂高限位")] public float LeftArmUpperPos; + [AsLowerIO(desc = "右夹臂低限位")] public float RightArmLowerPos; + [AsLowerIO(desc = "右夹臂高限位")] public float RightArmUpperPos; + + [AsUpperIO(desc = "从C往驱动器下使能")] public bool DisableFromC = false; + [AsUpperIO(desc = "从C上复位")] public bool ResetFromC = false; + [AsLowerIO(desc = "驱动轮使能状态")] public bool WheelAbleState = true; + + private float _multiVehicleAccumulateTh; + private DateTime _multiVehicleLastThTime = DateTime.Now; + private readonly object _multiVehicleNotificationLock = new(); + private DateTime _multiVehicleLastNotifyTime = DateTime.MinValue; + private bool _multiVehicleSyncInitialized; + private bool _multiVehicleWasActive; + private bool _multiVehicleMotionFeasible = true; + private string _multiVehicleMotionInfeasibleReason = ""; + private DateTime _multiVehicleStopLastLog = DateTime.MinValue; + private bool _multiVehicleRotateModeActive; + private float _multiVehicleRotateDirectionHint = 1f; + private string _multiVehicleRotateAlignDetail = ""; + private DateTime _multiVehicleRotateAlignLastLog = DateTime.MinValue; + private const float DefaultRotateCompTangentFrac = 0.10f; + + private struct RotateControlParams + { + public float ActiveOmega; + public float CompXyFac; + public float CompXyIFac; + public float CompXyMax; + public float CompThFac; + public float CompThIFac; + public float CompThMax; + public float CompTangentFrac; + public float StartWheelAlignDeg; + public float ActiveWheelAlignDeg; + } + + // 诊断日志:节流计时 + 最近一次检测几何(中心/朝向/距离),用于定位剧烈运动来源 + private DateTime _mvDbgLastLog = DateTime.MinValue; + private float _mvLastDetCenterX, _mvLastDetCenterY, _mvLastDetDir, _mvLastDetDist; + // 原地旋转(mode2)实际叠加的车体系纠偏旋量(mm/s, mm/s, deg/s),仅用于诊断日志。 + private float _mvRotCompVx, _mvRotCompVy, _mvRotCompOmega; + private DateTime _mvRotateCompLimitLastLog = DateTime.MinValue; + private bool _mvRotateCenterDriftActive; + private float _mvRotateCenterStartX, _mvRotateCenterStartY, _mvRotateCenterStartTh, _mvRotateCenterMaxDrift; + private DateTime _mvRotateCenterLastLog = DateTime.MinValue; + // 原地旋转纠偏 PI 控制器的积分累加器(mm·s, mm·s, deg·s)与上次计算时刻。 + private float _rotIntegX, _rotIntegY, _rotIntegTh; + private float _rotOmegaPeak; // 本次旋转过程中观测到的指令角速度峰值(deg/s),用于纠偏随转速缩放 + private DateTime _rotPiLastTime = DateTime.MinValue; + + // 原地旋转“两车实际 sim 位姿”诊断采样状态(仅主车,通过 Playground WebAPI 读取)。 + private DateTime _rotPoseLastLog = DateTime.MinValue; + private DateTime _rotPosePrevTime = DateTime.MinValue; + private float _rotPosePrevYaw1, _rotPosePrevYaw2; + private bool _rotPoseEpisode; // 本次旋转片段是否已捕获参考量 + private float _rotPoseMid0X, _rotPoseMid0Y; // 起转瞬间车队几何中心(世界系),用于度量整体平移漂移 + + // 写盘诊断日志(Clumsy 侧)。改用 DLog 落盘:同一 topic 的日志会归到以 topic 命名的文件夹, + // 由 DLog 统一管理落点/滚动,无需自行维护文件句柄与 session 清空逻辑。 + private DateTime _mvDiskLastLog = DateTime.MinValue; + private void FleetDiag(string msg) + { + DLog.Log($"car{CarNum} {msg}", "FleetDiagClumsy"); + } + + // 互识别:邻车两腿检测的滑动窗口(最近 1s,最多 10 帧),用于平滑抖动 + private DateTime _mvRemoteInputLastLog = DateTime.MinValue; + private DateTime _mvRemoteDecisionLastLog = DateTime.MinValue; + private DateTime _mvNotifyApplyLastLog = DateTime.MinValue; + private DateTime _mvRotateWheelOutputLastLog = DateTime.MinValue; + + private void LogMultiVehicleRemoteInput(bool isMaster, bool scriptOn, bool manualEnabled, bool autoEnabled, + int manualMode, float manualVx, float manualVy, float manualVth) + { + var now = DateTime.Now; + if ((now - _mvRemoteInputLastLog).TotalMilliseconds < 200) return; + _mvRemoteInputLastLog = now; + + int fleetCnt; + lock (FleetLock) fleetCnt = MultiVehicleFleet.Count; + var notifAgeMs = _multiVehicleLastNotifyTime == DateTime.MinValue + ? -1 + : (now - _multiVehicleLastNotifyTime).TotalMilliseconds; + var notifFresh = notifAgeMs >= 0 && notifAgeMs < Math.Max(300, Conf.MultiVehicleSyncInterval * 5); + + DLog.Log( + $"INPUT master={isMaster} endpoint={Conf.MultiVehicleMasterEndpoint} selfEndpoint={Conf.MultiVehicleSelfEndpoint} car={CarNum} " + + $"rawEn={MultiVehicleManualEnabled} rawHold={MultiVehicleHold} rawMode={MultiVehicleManualMode} rawVx={MultiVehicleManualVx:0.000} rawVy={MultiVehicleManualVy:0.000} rawVth={MultiVehicleManualVth:0.000} " + + $"scriptOn={scriptOn} effEn={manualEnabled} effMode={manualMode} effVx={manualVx:0.000} effVy={manualVy:0.000} effVth={manualVth:0.000} " + + $"auto={autoEnabled} notifFresh={notifFresh} notifAgeMs={notifAgeMs:0} fleetCnt={fleetCnt}/{Conf.MultiVehicleFleetNum}", + "MultiVehicleRemoteDbg"); + } + + private void LogMultiVehicleRemoteDecision(string msg, bool force = false) + { + var now = DateTime.Now; + if (!force && (now - _mvRemoteDecisionLastLog).TotalMilliseconds < 200) return; + _mvRemoteDecisionLastLog = now; + DLog.Log($"car{CarNum} {msg}", "MultiVehicleRemoteDbg"); + } + + private void LogRotateWheelOutputs(MultiWheelChassis chassis, string phase, float omega, bool force = false) + { + var now = DateTime.Now; + if (!force && (now - _mvRotateWheelOutputLastLog).TotalMilliseconds < 300) return; + _mvRotateWheelOutputLastLog = now; + +#pragma warning disable CS0612, CS0618 + var wheels = chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + var parts = new List(); + for (var i = 0; i < wheels.Count; i++) + { + var wheel = wheels[i]; + if (wheel is DiffSteerWheel diff) + { + parts.Add( + $"w{i}:tgt={diff.GetSendAngle():0.0} read={diff.ReadAngle():0.0} " + + $"L={diff.GetLeftSendSpeed():0.000} R={diff.GetRightSendSpeed():0.000}"); + } + else + { + parts.Add( + $"w{i}:tgt={wheel.GetSendAngle():0.0} read={wheel.ReadAngle():0.0} " + + $"v={wheel.GetSendSpeed():0.000}"); + } + } + + DLog.Log( + $"car{CarNum} phase={phase} omega={omega:0.000} " + string.Join(" | ", parts), + "MultiVehicleRotateWheelOutput"); + } + + private void SetMultiVehicleMotionFeasible(bool feasible, string reason = "") + { + _multiVehicleMotionFeasible = feasible; + _multiVehicleMotionInfeasibleReason = feasible ? "" : (reason ?? ""); + } + + private static float ClampFloat(float value, float min, float max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + private void UpdateMultiVehicleRotateModeState(int fleetMode, bool active, float requestedOmega, + RotateControlParams rotateParams) + { + var rotateActive = active && fleetMode == 2; + if (!rotateActive) + { + _multiVehicleRotateModeActive = false; + if (!active) + { + MultiVehicleRotateWheelsReady = true; + _multiVehicleRotateAlignDetail = ""; + } + MultiVehicleRotateFleetReady = true; + _multiVehicleRotateDirectionHint = 1f; + return; + } + + if (!_multiVehicleRotateModeActive) + { + MultiVehicleRotateWheelsReady = false; + MultiVehicleRotateFleetReady = false; + _multiVehicleRotateAlignDetail = "rotate mode just entered"; + } + + _multiVehicleRotateModeActive = true; + if (Math.Abs(requestedOmega) > rotateParams.ActiveOmega) + _multiVehicleRotateDirectionHint = Math.Sign(requestedOmega); + } + + private bool PrepareMultiVehicleRotateWheels(MultiWheelChassis chassis, float requestedOmega, + RotateControlParams rotateParams, + float localCompensateX = 0f, float localCompensateY = 0f, float localCompensateTh = 0f, + bool rampStop = true) + { + var hint = Math.Abs(requestedOmega) > rotateParams.ActiveOmega + ? Math.Sign(requestedOmega) + : Math.Sign(_multiVehicleRotateDirectionHint); + if (hint == 0) hint = 1; + _multiVehicleRotateDirectionHint = hint; + + // Reuse SendRotateMotion's decomposition and angle-limit checks, but keep wheel speed at zero. + if (rampStop) + chassis.RampStop(); + var alignOmega = Math.Abs(requestedOmega) > rotateParams.ActiveOmega + ? requestedOmega + : 0.01f * hint; + var motionOk = chassis.SendRotateMotion(alignOmega, TimeSpan.Zero, + localCompensateX: localCompensateX, localCompensateY: localCompensateY, + localCompensateTh: localCompensateTh); + var wheelAligned = TryCheckRotateWheelAlignment(chassis, rotateParams.StartWheelAlignDeg, + out var alignDetail); + var aligned = motionOk && wheelAligned; + if (!motionOk) + alignDetail = string.IsNullOrWhiteSpace(chassis.LastMotionDecomposeFailureReason) + ? alignDetail + : chassis.LastMotionDecomposeFailureReason; + + MultiVehicleRotateWheelsReady = aligned; + _multiVehicleRotateAlignDetail = alignDetail; + + var now = DateTime.Now; + if ((now - _multiVehicleRotateAlignLastLog).TotalMilliseconds >= 200) + { + _multiVehicleRotateAlignLastLog = now; + DLog.Log( + $"car{CarNum} ROTATE_PREPARE omegaReq={requestedOmega:0.000} alignOmega={alignOmega:0.000} " + + $"hint={hint} comp=({localCompensateX:0.0},{localCompensateY:0.0},{localCompensateTh:0.000}) " + + $"rampStop={rampStop} startTol={rotateParams.StartWheelAlignDeg:0.0} ok={motionOk} aligned={aligned} detail={alignDetail} " + + $"chassisReason={chassis.LastMotionDecomposeFailureReason}", + "MultiVehicleRemoteDbg"); + FleetDiag( + $"ROTATE_PREPARE omegaReq={requestedOmega:0.000} alignOmega={alignOmega:0.000} hint={hint} " + + $"comp=({localCompensateX:0.0},{localCompensateY:0.0},{localCompensateTh:0.000}) " + + $"startTol={rotateParams.StartWheelAlignDeg:0.0} ok={motionOk} aligned={aligned} detail={alignDetail}"); + } + + Hedingben.ToastText( + aligned ? "车队原地旋转舵轮已对齐" : $"车队原地旋转舵轮预对齐中 {alignDetail}", + $"MultiVehicle{CarNum}-rotate-align"); + + return motionOk; + } + + private static bool TryCheckRotateWheelAlignment(MultiWheelChassis chassis, float toleranceDeg, out string detail) + { + try + { +#pragma warning disable CS0612, CS0618 + var wheels = chassis.GetSteerWheels(); +#pragma warning restore CS0612, CS0618 + var maxDth = 0f; + var parts = new List(); + for (var i = 0; i < wheels.Count; i++) + { + var sw = wheels[i]; + var dth = Math.Abs(CommonMath.ThDiff(sw.ReadAngle(), sw.GetSendAngle())); + maxDth = Math.Max(maxDth, dth); + parts.Add($"w{i}:{dth:0.0}"); + } + + detail = $"maxDth={maxDth:0.0} tol={toleranceDeg:0.0} {string.Join(",", parts)}"; + return maxDth <= toleranceDeg; + } + catch (Exception e) + { + detail = $"read alignment failed: {e.Message}"; + return chassis.LastRotateAligned; + } + } + + private void LogMultiVehicleStop(string reason, bool force = false) + { + var now = DateTime.Now; + if (!force && (now - _multiVehicleStopLastLog).TotalMilliseconds < 500) return; + _multiVehicleStopLastLog = now; + var msg = $"FLEET_STOP car={CarNum} reason={reason}"; + DLog.Log(msg, "MultiVehicleSafety"); + FleetDiag(msg); + Hedingben.ToastText($"Fleet stop: {reason}", $"MultiVehicle{CarNum}-stop"); + } + + private readonly object _neighborDetectLock = new(); + private readonly List<(DateTime Time, Vector2 Src, Vector2 Dst)> _neighborDetects = new(); + + // 互识别:上一帧检测中心,用作下一帧猜测(闭环跟踪),与「多舵轮-2腿检测」MovementTest 一致 + private Vector2 _neighborGuess; + private bool _neighborGuessValid; + private DateTime _neighborGuessTime = DateTime.MinValue; + + /// + /// 互识别钩子:用单线雷达识别邻车的两腿/两轮,换算成本车体坐标系下相对“理应对齐参考点”的偏差。 + /// 全对齐时返回 (true, 0, 0, 0);未检测到时返回 (false, ...)。 + /// detectDistance = 编队车间距 - 检测中心偏移(即邻车检测中心到本车原点的标称距离,恒为正值,方向由 TwoLegGuessX 符号决定)。 + /// + private (bool Valid, float Dx, float Dy, float Dth, float NeighborDist) DetectNeighborBias(float detectDistance) + { + // 检测猜测点与「多舵轮-2腿检测」MovementTest 完全一致:首帧 / 跟丢回落都用 Conf.TwoLegGuessX + // (方向与距离都由它一处决定),跟到后用上一帧中心闭环跟踪。 + // detectDistance 只用于计算编队对齐偏差/间距,不参与“去哪里找邻车”,避免猜测点偏离导致 ROI 框漏掉真实邻车。 + var nominal = new Vector2(Conf.TwoLegGuessX, 0f); + + var now = DateTime.Now; + // 闭环跟踪:1s 内检到过就用上一帧中心作猜测,使绿色 ROI 框收敛到真实邻车(与 MovementTest 行为一致); + // 跟丢超时则回落到编队标称位置,避免框漂走。 + var guess = (_neighborGuessValid && (now - _neighborGuessTime).TotalSeconds < 1) ? _neighborGuess : nominal; + var seg = TwoLegDetect.Detect(Conf.TwoLegLidarName, guess.X, TwoLegDetect.SetFilters(guess.X, guess.Y)); + + Vector2 avgSrc, avgDst; + lock (_neighborDetectLock) + { + if (seg != null) + { + _neighborDetects.Add((now, seg.Src, seg.Dst)); + _neighborGuess = (seg.Src + seg.Dst) / 2f; + _neighborGuessValid = true; + _neighborGuessTime = now; + } + _neighborDetects.RemoveAll(r => (now - r.Time).TotalSeconds > 1); + + var cnt = _neighborDetects.Count; + if (cnt == 0) + { + _neighborGuessValid = false; + Hedingben.ToastText($"邻车未检到 (guess x:{guess.X:F0} y:{guess.Y:F0})", $"MultiVehicle{CarNum}-detect"); + return (false, 0f, 0f, 0f, 0f); + } + if (cnt > 10) _neighborDetects.RemoveRange(0, cnt - 10); + + avgSrc = new Vector2(_neighborDetects.Average(r => r.Src.X), _neighborDetects.Average(r => r.Src.Y)); + avgDst = new Vector2(_neighborDetects.Average(r => r.Dst.X), _neighborDetects.Average(r => r.Dst.Y)); + } + + // 两腿连线中点为邻车检测中心;连线的垂线方向即邻车朝向 + var dirVec = avgDst - avgSrc; + var center = (avgSrc + avgDst) / 2f; + var dir = (float)LessMath.RoundTh(Math.Atan2(dirVec.Y, dirVec.X) / Math.PI * 180 + 90); + + // 从检测中心沿邻车朝向外推 detectDistance,得到“本车理应所在的参考点”(车体系) + var target = LessMath.Transform2D(center, dir, detectDistance, 0); + + var dx = target.X; + var dy = target.Y; + var dth = (float)LessMath.ThDiff(dir, 0); + var neighborDist = center.Length(); + + _mvLastDetCenterX = center.X; + _mvLastDetCenterY = center.Y; + _mvLastDetDir = dir; + _mvLastDetDist = neighborDist; + + var painter = UI.GetPainter("MultiVehicleDetectBias", false); + painter.Clear(); + painter.DrawLine(Color.DeepPink, center, target, width: 2); + painter.DrawText(Color.Yellow, $"dx:{dx:F0} dy:{dy:F0} dth:{dth:F2}", center.X, center.Y); + + Hedingben.ToastText( + $"[联动检测] lidar:{Conf.TwoLegLidarName} guess x:{guess.X:F0} y:{guess.Y:F0} | " + + $"中心 x:{center.X:F0} y:{center.Y:F0} dir:{dir:F1} dist:{neighborDist:F0} | 偏差 dx:{dx:F0} dy:{dy:F0} dth:{dth:F2}", + $"MultiVehicle{CarNum}-detect"); + + return (true, dx, dy, dth, neighborDist); + } + + private void InitMultiVehicleCoordination() + { + if (_multiVehicleSyncInitialized) return; + _multiVehicleSyncInitialized = true; + + var hc = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + + PicoHttpServer.AddPostByteHandler("/multi-vehicle-register-bin", body => + { + try + { + var (carNum, info) = VehicleSyncBinaryCodec.DecodeRegister(body); + ApplyVehicleSyncRegister(carNum, info); + return "ok"; + } + catch (Exception e) + { + DLog.Log($"/multi-vehicle-register-bin error: {e.FormatEx()}", "MultiVehicle"); + throw; + } + }); + + PicoHttpServer.AddPostByteHandler("/multi-vehicle-notify-bin", body => + { + try + { + var notification = VehicleSyncBinaryCodec.DecodeNotification(body); + return ApplyVehicleSyncNotification(notification); + } + catch (Exception e) + { + DLog.Log($"/multi-vehicle-notify-bin error: {e.FormatEx()}", "MultiVehicle"); + throw; + } + }); + + DLog.Log("MultiVehicle coordination initialized, starting loop thread", "MultiVehicle"); + Console.WriteLine("[MultiVehicle] coordination initialized, starting loop thread"); + new Thread(() => MultiVehicleLoop(hc)) { Name = "MultiVehicle", IsBackground = true }.Start(); + } + + private void ApplyVehicleSyncRegister(int carNum, VehicleSyncInfo info) + { + lock (FleetLock) + { + MultiVehicleFleet[carNum] = info; + _multiVehicleFleetSeen[carNum] = DateTime.Now; + } + } + + private string ApplyVehicleSyncNotification(VehicleSyncNotification notification) + { + // Keep the same stale-packet semantics as the previous transport. + if (notification.Seq != 0 && notification.Seq <= _multiVehicleAppliedSeq && + notification.Seq > _multiVehicleAppliedSeq - 100) + return "stale"; + _multiVehicleAppliedSeq = notification.Seq; + + MultiVehicleAligned = notification.Aligned; + lock (FleetLock) + { + MultiVehicleFleet = notification.Fleet ?? new Dictionary(); + var nowSeen = DateTime.Now; + foreach (var key in MultiVehicleFleet.Keys) + _multiVehicleFleetSeen[key] = nowSeen; + } + + lock (_multiVehicleNotificationLock) + { + MultiVehicleNotification = notification; + _multiVehicleLastNotifyTime = DateTime.Now; + } + + var applyNow = DateTime.Now; + if ((applyNow - _mvNotifyApplyLastLog).TotalMilliseconds >= 200) + { + _mvNotifyApplyLastLog = applyNow; + DLog.Log( + $"car{CarNum} NOTIFY_APPLY seq={notification.Seq} mode={notification.Mode} " + + $"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " + + $"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " + + $"reason={notification.FleetStopReason} useDetourCorr={notification.UseDetourCorrection} " + + $"rotParam(valid={notification.RotateParamsValid} xyP={notification.RotateCompXyFac:0.###} " + + $"xyI={notification.RotateCompXyIFac:0.###} xyMax={notification.RotateCompXyMax:0.#} " + + $"thP={notification.RotateCompThFac:0.###} thI={notification.RotateCompThIFac:0.###} " + + $"thMax={notification.RotateCompThMax:0.#} frac={notification.RotateCompTangentFrac:0.###} " + + $"startTol={notification.RotateStartWheelAlignDeg:0.#} activeTol={notification.RotateActiveWheelAlignDeg:0.#})", + "MultiVehicleRemoteDbg"); + } + + return "ok"; + } + + private void MultiVehicleLoop(HttpClient hc) + { + var carLength = CarLength; + var carWidth = CarWidth; + var contour = new List + { + new(carLength / 2f, carWidth / 2f), + new(-carLength / 2f, carWidth / 2f), + new(-carLength / 2f, -carWidth / 2f), + new(carLength / 2f, -carWidth / 2f), + }; + var chassis = (MultiWheelChassis)Chassis; + var sendMotionPainter = UI.GetPainter("MultiWheelChassis-SendMotionVis", false); + chassis.SendMotionVisualizer = new Visualizer + { + LineAction = (color, src, dst, startArrow, endArrow, width) => + sendMotionPainter.DrawLine(color, src, dst, startArrow, endArrow, width), + TextAction = (color, str, pos) => sendMotionPainter.DrawText(color, str, pos), + Clear = () => sendMotionPainter.Clear() + }; + + DLog.Log("MultiVehicleLoop thread running", "MultiVehicle"); + Console.WriteLine("[MultiVehicle] loop thread running"); + var loopCount = 0L; + while (true) + { + try + { + TickMultiVehicle(hc, chassis, contour, sendMotionPainter); + } + catch (Exception e) + { + DLog.Log($"MultiVehicle loop error: {e.FormatEx()}", "MultiVehicle"); + } + + // 每 ~5s 打一条心跳,确认循环确实在跑(用于区分“循环没跑”与“写盘失败”) + if (loopCount++ % Math.Max(1, 5000 / Math.Max(1, Conf.MultiVehicleSyncInterval)) == 0) + DLog.Log($"MultiVehicleLoop heartbeat #{loopCount}", "MultiVehicle"); + + Thread.Sleep(Conf.MultiVehicleSyncInterval); + } + } + + private void TickMultiVehicle(HttpClient hc, MultiWheelChassis chassis, List contour, Painter sendMotionPainter) + { + var isMaster = IsMultiVehicleMaster(); + var autoEnabled = false; + // 手动等价输入 = Medulla 遥控(IO) 或 Clumsy 脚本/动作驱动(二者择一,脚本优先)。 + var scriptOn = MultiVehicleScriptEnabled; + var manualEnabled = MultiVehicleManualEnabled || scriptOn; + // 主车实际生效的手动模式与速度:脚本使能时取脚本字段,否则取 Medulla 手动 IO 字段。 + var manualMode = scriptOn ? MultiVehicleScriptMode : MultiVehicleManualMode; + var manualVx = scriptOn ? MultiVehicleScriptVx : MultiVehicleManualVx; + var manualVy = scriptOn ? MultiVehicleScriptVy : MultiVehicleManualVy; + var manualVth = scriptOn ? MultiVehicleScriptVth : MultiVehicleManualVth; + var manualHold = !scriptOn && MultiVehicleHold; + VehicleSyncNotification activeNotification = null; + + if (isMaster) + autoEnabled = MultiVehicleAutoEnabled; + else + { + // 从车:自动/手动联动均可由主车广播解锁(无需各自再拨开关)。 + // 仅在 notification 新鲜时认账,主车停发后超时即自动停车,避免用旧指令跑飞。 + lock (_multiVehicleNotificationLock) + { + var fresh = (DateTime.Now - _multiVehicleLastNotifyTime).TotalMilliseconds < + Math.Max(300, Conf.MultiVehicleSyncInterval * 5); + if (fresh && MultiVehicleNotification != null) + { + activeNotification = MultiVehicleNotification; + autoEnabled = MultiVehicleNotification.AutoEnabled; + manualEnabled = manualEnabled || MultiVehicleNotification.ManualEnabled; + } + } + } + var rotateParams = BuildRotateControlParams(isMaster ? null : activeNotification); + + // 入口诊断(节流 ~300ms):记录从 Medulla 收到的原始 IO 值与门控判定, + // 用于确认遥控指令是否真的传到了 Clumsy,以及为何提前 return。 + LogMultiVehicleRemoteInput(isMaster, scriptOn, manualEnabled, autoEnabled, + manualMode, manualVx, manualVy, manualVth); + + if ((DateTime.Now - _mvDiskLastLog).TotalMilliseconds >= 300) + { + _mvDiskLastLog = DateTime.Now; + int fleetCnt; + lock (FleetLock) fleetCnt = MultiVehicleFleet.Count; + var notifFresh = (DateTime.Now - _multiVehicleLastNotifyTime).TotalMilliseconds < + Math.Max(300, Conf.MultiVehicleSyncInterval * 5); + FleetDiag( + $"ENTRY master={isMaster} | IO: ManualEn={MultiVehicleManualEnabled} Mode={MultiVehicleManualMode} " + + $"Hold={MultiVehicleHold} Vx={MultiVehicleManualVx:0.000} Vy={MultiVehicleManualVy:0.000} Vth={MultiVehicleManualVth:0.0} AutoEn(IO)={MultiVehicleAutoEnabled} " + + $"| SCRIPT en={MultiVehicleScriptEnabled} mode={MultiVehicleScriptMode} Vx={MultiVehicleScriptVx:0.000} Vy={MultiVehicleScriptVy:0.000} Vth={MultiVehicleScriptVth:0.0} " + + $"| gate: manualEnabled={manualEnabled} autoEnabled={autoEnabled} notifFresh={notifFresh} " + + $"notifManualEn={(MultiVehicleNotification != null ? MultiVehicleNotification.ManualEnabled.ToString() : "null")} " + + $"fleetCnt={fleetCnt}/{Conf.MultiVehicleFleetNum} useDetect={Conf.MultiVehicleUseDetect} " + + $"useDetour={Conf.MultiVehicleSyncUseDetour} manualDetour={Conf.MultiVehicleManualUseDetourCorrection}"); + } + + if (!manualEnabled && !autoEnabled) + { + if (_multiVehicleWasActive) + { + chassis.RampStop(); + SetMultiVehicleMotionFeasible(true); + LogMultiVehicleStop("fleet control disabled; ramp stop previous fleet command", true); + } + _multiVehicleWasActive = false; + UpdateMultiVehicleRotateModeState(0, false, 0, rotateParams); + ResetMultiVehicleRotateCenterDrift(); + LogMultiVehicleRemoteDecision( + $"RETURN_IDLE master={isMaster} rawEn={MultiVehicleManualEnabled} scriptOn={scriptOn} auto={autoEnabled} " + + $"rawHold={MultiVehicleHold} rawMode={MultiVehicleManualMode} rawVx={MultiVehicleManualVx:0.000} rawVy={MultiVehicleManualVy:0.000} rawVth={MultiVehicleManualVth:0.000}"); + UI.GetPainter("MultiVehicleFleet-vis", false).Clear(); + sendMotionPainter.Clear(); + lock (FleetLock) + { + MultiVehicleFleet.Clear(); + _multiVehicleFleetSeen.Clear(); + } + MultiVehicleAutoEnabled = false; + MultiVehicleAutoHasIdeal = false; + _multiVehicleAppliedSeq = -1; + _multiVehicleAccumulateTh = 0f; + _multiVehicleLastThTime = DateTime.Now; + + if (!isMaster) + FireAndForgetRegister(hc, BuildSelfInfo(false, false, 0, 0, 0, 0, 0, 0, false)); + + return; + } + _multiVehicleWasActive = true; + + // 自动模式整队姿态依赖 Detour 全局定位(与 MultiVehicleSyncUseDetour 无关):要求编队至少一台车有定位。 + if (isMaster && autoEnabled && !manualEnabled && !FleetHasPosAvailable()) + { + MultiVehicleAutoEnabled = false; + chassis.RampStop(); + SetMultiVehicleMotionFeasible(true); + LogMultiVehicleStop("auto mode requires at least one Detour-positioned fleet member", true); + Hedingben.ToastText("自动多车联动需要至少一台车有 Detour 定位", "MultiVehicle-auto-gate"); + return; + } + + // 两类用途解耦(关键语义): + // - useDetourCorrection (= MultiVehicleSyncUseDetour):是否用 SLAM 位姿做"车队内姿态纠正"(POS 补偿)。 + // false 仅表示"定位不参与车队内姿态纠正",不影响下面整队姿态计算。 + // - slamRead:本车本轮是否读取 Detour 全局位姿。"整个车队姿态的计算"(主车反推/广播车队中心、 + // SLAM 间距、自动模式安全门)始终依赖全局定位 —— 故自动模式下主车必读,与开关无关; + // 手动外部遥控默认不读 Detour,避免 getCartLocation 阻塞拖慢 2 腿检测;脚本自动原地旋转 + // (FleetRotateInPlace) 已经依赖 Detour 判停,因此显式打开 POS 纠偏以保持旋转中心。 + var autoMode = autoEnabled && !manualEnabled; + var scriptRotateDetourCorrection = isMaster && scriptOn && manualMode == 2 && Conf.FleetRotateUseDetourHeading; + var notificationDetourCorrection = !isMaster && activeNotification != null && + activeNotification.UseDetourCorrection; + var manualDetourCorrection = manualEnabled && + (Conf.MultiVehicleManualUseDetourCorrection || + scriptRotateDetourCorrection || + notificationDetourCorrection); + var useDetourCorrection = Conf.MultiVehicleSyncUseDetour && (!manualEnabled || manualDetourCorrection); + var slamRead = useDetourCorrection || (isMaster && autoMode); + float selfX = 0, selfY = 0, selfTh = 0; + if (slamRead) + { + var carPos = DetourInterface.getCartLocation(); + selfX = (float)carPos.x; + selfY = (float)carPos.y; + selfTh = (float)carPos.th; + } + // fleetPosValid:整队全局姿态是否已知。主车=自身读到 SLAM;从车=主车广播标志(下方覆盖)。 + var fleetPosValid = slamRead; + + var syncTh = Conf.TestCarSyncTh; + var syncDistance = Conf.TestCarSyncDistance; + var deltaDetectCenter = Conf.DeltaDetectCenter; + float fleetVx = 0, fleetFrontTh = 0, fleetRearTh = 0, fleetOmega = 0; + var fleetMode = 0; // 0=常规 1=蟹行 2=原地旋转 + var autoCommandTimedOut = false; + var notificationStopActive = false; + var notificationStopReason = ""; + var notificationStopSourceCar = 0; + var notificationRequestedFleetOmega = 0f; + var notificationMotionReleased = true; + float crabInputVx = 0, crabInputVy = 0, crabRawAngle = 0; + float crabSteerLimit = 0; + var crabReverseEquivalent = false; + + if (isMaster) + { + if (manualEnabled) + { + syncTh = 0; + fleetMode = manualMode; + var remoteRatioInput = !scriptOn; + if (fleetMode == 2) + { + // 外部遥控: Vth 为摇杆比例;内部脚本: Vth 为实际角速度(deg/s)。 + fleetOmega = remoteRatioInput + ? ClampFloat(manualVth, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxRotateOmegaDegPerSec) + : manualVth; + fleetVx = 0; + fleetFrontTh = 0; + fleetRearTh = 0; + _multiVehicleAccumulateTh = 0f; + _multiVehicleLastThTime = DateTime.Now; + } + else if (fleetMode == 1) + { + // 外部遥控: Vx/Vy 为摇杆比例;内部脚本: Vx 为线速度(m/s), Vy 为蟹行舵角(deg)。 + var speed = remoteRatioInput + ? ClampFloat(manualVx, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxSpeed) + : manualVx; + var crabRatio = remoteRatioInput ? ClampFloat(manualVy, -1f, 1f) : 0f; + var crabAngle = remoteRatioInput + ? crabRatio * Math.Abs(Conf.FleetManualMaxCrabAngleDeg) + : manualVy; + crabInputVx = speed; + crabInputVy = remoteRatioInput ? crabRatio : crabAngle; + crabRawAngle = crabAngle; + crabSteerLimit = Math.Min(179f, Math.Max(1f, Math.Abs(Conf.MultiVehicleCrabSteerLimitDeg))); + + if (crabAngle > crabSteerLimit) { crabAngle -= 180f; speed = -speed; crabReverseEquivalent = true; } + else if (crabAngle < -crabSteerLimit) { crabAngle += 180f; speed = -speed; crabReverseEquivalent = true; } + fleetVx = speed; + fleetFrontTh = crabAngle; + fleetRearTh = crabAngle; + _multiVehicleAccumulateTh = 0f; + _multiVehicleLastThTime = DateTime.Now; + } + else + { + // 外部遥控: Vx/Vth 为摇杆比例;内部脚本: Vx 为线速度(m/s), Vth 为目标舵角(deg)。 + fleetVx = remoteRatioInput + ? ClampFloat(manualVx, -1f, 1f) * Math.Max(0f, Conf.FleetManualMaxSpeed) + : manualVx; + var targetTh = remoteRatioInput + ? ClampFloat(manualVth, -1f, 1f) * Math.Abs(Conf.FleetManualMaxSteerAngleDeg) + : manualVth; + var now = DateTime.Now; + var dt = (float)Math.Min(0.2, Math.Max(0, (now - _multiVehicleLastThTime).TotalSeconds)); + _multiVehicleLastThTime = now; + var sign = Math.Sign(targetTh - _multiVehicleAccumulateTh); + _multiVehicleAccumulateTh += sign * Math.Min(Math.Abs(targetTh - _multiVehicleAccumulateTh), + Conf.SyncThAccPerSec * dt); + fleetFrontTh = _multiVehicleAccumulateTh; + fleetRearTh = -fleetFrontTh; + } + + if (manualHold) + { + fleetMode = 0; + fleetVx = 0; + fleetFrontTh = 0; + fleetRearTh = 0; + fleetOmega = 0; + _multiVehicleAccumulateTh = 0f; + _multiVehicleLastThTime = DateTime.Now; + } + } + else + { + // B: 自动模式速度命令新鲜度门控。路径结束/回调早退/卡顿后,控制器不再刷新 AutoCmdTime, + // 超时即视为失效:清零下发速度与 idealPos,关闭 AutoEnabled,避免车队按末速度滑行。 + var autoTimeoutMs = Conf.MultiVehicleAutoCmdTimeoutMs > 0 + ? Conf.MultiVehicleAutoCmdTimeoutMs + : Math.Max(200, Conf.MultiVehicleSyncInterval * 4); + var autoFresh = (DateTime.Now - MultiVehicleAutoCmdTime).TotalMilliseconds < autoTimeoutMs; + if (autoFresh) + { + fleetVx = MultiVehicleAutoVx; + fleetFrontTh = MultiVehicleAutoFrontTh; + fleetRearTh = MultiVehicleAutoRearTh; + } + else + { + autoCommandTimedOut = true; + fleetVx = fleetFrontTh = fleetRearTh = 0; + MultiVehicleAutoVx = MultiVehicleAutoFrontTh = MultiVehicleAutoRearTh = 0; + MultiVehicleAutoHasIdeal = false; + MultiVehicleAutoEnabled = false; + Hedingben.ToastText("自动速度命令超时,已停车(路径结束/控制器停发)", "MultiVehicle-auto-timeout"); + } + } + } + else if (activeNotification != null) + { + var notification = activeNotification; + + syncTh = notification.SyncTh; + syncDistance = notification.SyncDistance; + deltaDetectCenter = notification.DeltaDetectCenter; + PublishFleetCenter(notification.CenterX, notification.CenterY, notification.CenterTh); + // 从车整队姿态来自主车广播:以广播标志作为 fleetPosValid(自身 slamRead 仅决定是否做本车纠偏)。 + fleetPosValid = notification.PosAvailable; + MultiVehicleAutoEnabled = notification.AutoEnabled; + fleetVx = notification.FleetVx; + fleetFrontTh = notification.FleetFrontTh; + fleetRearTh = notification.FleetRearTh; + fleetMode = notification.Mode; + fleetOmega = notification.FleetOmega; + notificationRequestedFleetOmega = notification.RequestedFleetOmega; + notificationMotionReleased = notification.FleetMotionReleased; + notificationStopActive = notification.FleetStopActive; + notificationStopReason = notification.FleetStopReason ?? ""; + notificationStopSourceCar = notification.FleetStopSourceCar; + // D: 从车采用主车广播的理想车队中心(弧线时由 idealPos/idealAngle 而来)做前馈目标。 + MultiVehicleAutoHasIdeal = notification.HasIdeal; + MultiVehicleAutoIdealX = notification.IdealX; + MultiVehicleAutoIdealY = notification.IdealY; + MultiVehicleAutoIdealTh = notification.IdealTh; + } + + var requestedFleetOmega = !isMaster && fleetMode == 2 + ? ((Math.Abs(notificationRequestedFleetOmega) > 1e-6f || !notificationMotionReleased) + ? notificationRequestedFleetOmega + : fleetOmega) + : fleetOmega; + UpdateMultiVehicleRotateModeState(fleetMode, manualEnabled || autoEnabled, requestedFleetOmega, rotateParams); + + var (layoutX, layoutY, layoutTh) = GetLayoutPose(syncTh, syncDistance); + var inferredFleetCenterValid = false; + float inferredFleetCenterX = 0, inferredFleetCenterY = 0, inferredFleetCenterTh = 0; + Tuple supposedPosForDiag = null; + + if (isMaster) + { + lock (FleetLock) + { + MultiVehicleFleet[CarNum] = BuildSelfInfo(true, slamRead, selfX, selfY, selfTh, + layoutX, layoutY, layoutTh, true); + _multiVehicleFleetSeen[CarNum] = DateTime.Now; // 主车自身恒新鲜 + PruneStaleFleetMembers(); // C: 剔除掉线从车 + } + + if (TryInferFleetCenter(out var cx, out var cy, out var cth)) + { + inferredFleetCenterValid = true; + inferredFleetCenterX = cx; + inferredFleetCenterY = cy; + inferredFleetCenterTh = cth; + PublishFleetCenter(cx, cy, cth); + } + + // D: 自动模式(非手动)下,若控制器给出理想车队中心,则以理想位姿作为各车 layout 目标, + // 使弧线路径上从车按各自相对曲率中心位置前馈,而非仅靠事后检测/SLAM 纠偏。 + if (!manualEnabled && MultiVehicleAutoEnabled && + Conf.MultiVehicleAutoUseIdealCenter && MultiVehicleAutoHasIdeal) + PublishFleetCenter(MultiVehicleAutoIdealX, MultiVehicleAutoIdealY, MultiVehicleAutoIdealTh); + } + + var selfAligned = !Conf.MultiVehicleUseDetect; + var detectValid = false; + float detectDx = 0, detectDy = 0, detectDth = 0; + var currentSpacing = float.NaN; + if (Conf.MultiVehicleUseDetect) + { + var (valid, dx, dy, dth, neighborDist) = DetectNeighborBias(syncDistance - deltaDetectCenter); + detectValid = valid; + if (valid) + { + detectDx = dx; + detectDy = dy; + detectDth = dth; + // 检测中心到本车原点距离 + 检测中心偏移 = 两车参考点当前间距 + currentSpacing = neighborDist + deltaDetectCenter; + selfAligned = Math.Abs(dx) < Conf.SingleCarSyncPrecisionXy && + Math.Abs(dy) < Conf.SingleCarSyncPrecisionXy && + Math.Abs(dth) < Conf.SingleCarSyncPrecisionTh; + } + else selfAligned = false; + } + + // 检测不可用(关闭/跟丢)时回落到 SLAM 世界坐标计算间距(需本车已读到全局定位) + if (float.IsNaN(currentSpacing) && slamRead) + currentSpacing = TryGetSlamSpacing(selfX, selfY); + + if (float.IsNaN(currentSpacing)) + Hedingben.ToastText($"间距 当前:N/A 目标:{syncDistance:F0}mm", $"MultiVehicle{CarNum}-spacing"); + else + Hedingben.ToastText( + $"间距 当前:{currentSpacing:F0}mm 目标:{syncDistance:F0}mm 差:{currentSpacing - syncDistance:F0}mm", + $"MultiVehicle{CarNum}-spacing"); + + lock (FleetLock) + { + MultiVehicleAligned = MultiVehicleFleet.Count == Conf.MultiVehicleFleetNum && + MultiVehicleFleet.Values.All(v => v.Aligned); + } + + var fleetReady = false; + var fleetCount = 0; + lock (FleetLock) + { + fleetCount = MultiVehicleFleet.Count; + fleetReady = fleetCount == Conf.MultiVehicleFleetNum; + } + + // 安全门:开启互识别时,本车或任一其它车检测不到邻车则整队停车(速度置零)。 + // ownDetectOk 是本轮新鲜值;其它车的 DetectOk 来自其上报/主车下发(滑动窗口已给 1s 去抖)。 + var ownDetectOk = !Conf.MultiVehicleUseDetect || detectValid; + bool othersDetectOk; + string otherDetectLostCars; + List> motionInfeasibleMembers; + List> rotateUnalignedMembers; + lock (FleetLock) + { + othersDetectOk = MultiVehicleFleet.Where(kv => kv.Key != CarNum).All(kv => kv.Value.DetectOk); + otherDetectLostCars = string.Join(",", MultiVehicleFleet + .Where(kv => kv.Key != CarNum && !kv.Value.DetectOk) + .Select(kv => kv.Key.ToString())); + motionInfeasibleMembers = MultiVehicleFleet + .Where(kv => kv.Key != CarNum && !kv.Value.MotionFeasible) + .ToList(); + rotateUnalignedMembers = MultiVehicleFleet + .Where(kv => !kv.Value.RotateWheelsAligned) + .ToList(); + } + if (!MultiVehicleRotateWheelsReady && rotateUnalignedMembers.All(kv => kv.Key != CarNum)) + { + rotateUnalignedMembers.Add(new KeyValuePair(CarNum, new VehicleSyncInfo + { + RotateWheelsAligned = false, + RotateWheelAlignDetail = _multiVehicleRotateAlignDetail + })); + } + var rotateUnalignedCars = string.Join(",", rotateUnalignedMembers.Select(kv => kv.Key.ToString())); + var rotateFleetWheelsAligned = fleetMode != 2 || (fleetReady && rotateUnalignedMembers.Count == 0); + MultiVehicleRotateFleetReady = rotateFleetWheelsAligned; + + var fleetStopActive = false; + var fleetStopReason = ""; + var fleetStopSourceCar = 0; + void AddFleetStop(string reason, int sourceCar = 0) + { + if (string.IsNullOrWhiteSpace(reason)) return; + if (!fleetStopActive) + { + fleetStopReason = reason; + fleetStopSourceCar = sourceCar; + } + else + { + fleetStopReason += " | " + reason; + if (fleetStopSourceCar == 0) fleetStopSourceCar = sourceCar; + } + fleetStopActive = true; + } + + if (!fleetReady) + AddFleetStop($"member not ready/stale fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); + if (autoCommandTimedOut) + AddFleetStop("auto command timeout"); + if (Conf.MultiVehicleUseDetect && !ownDetectOk) + AddFleetStop("own two-leg detection lost", CarNum); + if (Conf.MultiVehicleUseDetect && !othersDetectOk) + AddFleetStop($"other two-leg detection lost cars=[{otherDetectLostCars}]"); + if (notificationStopActive) + AddFleetStop($"master stop from car{notificationStopSourceCar}: {notificationStopReason}", + notificationStopSourceCar); + if (!_multiVehicleMotionFeasible) + AddFleetStop($"car{CarNum} motion infeasible: {_multiVehicleMotionInfeasibleReason}", CarNum); + foreach (var kv in motionInfeasibleMembers) + AddFleetStop($"car{kv.Key} motion infeasible: {kv.Value.MotionInfeasibleReason}", kv.Key); + + var canMove = !fleetStopActive; + + // H: 自动模式(非手动)必须有有效车队中心——主车由 SLAM 反推、从车依赖主车广播 fleetPosValid。 + // 自动模式整队姿态始终依赖 Detour(与 MultiVehicleSyncUseDetour 无关):定位全程丢失时强制停车。 + // (用 Detour 时若定位丢失,getCartLocation() 已先行阻塞,此处再兜底要求有效车队中心。) + // 手动模式不受限(允许仅靠互识别/遥控行驶)。 + if (canMove && autoMode && Conf.MultiVehicleAutoRequireFleetCenter) + { + var fleetCenterValid = fleetPosValid && (!isMaster || TryInferFleetCenter(out _, out _, out _)); + if (!fleetCenterValid) + { + AddFleetStop("auto fleet center invalid/localization lost", CarNum); + canMove = false; + Hedingben.ToastText("自动模式无有效车队中心(定位丢失),已停车", $"MultiVehicle{CarNum}-autostop"); + } + } + + if (!canMove) + { + fleetVx = 0; + fleetFrontTh = 0; + fleetRearTh = 0; + fleetOmega = 0; + Hedingben.ToastText( + $"车队停车:{(!ownDetectOk ? "本车" : "其它车")}2腿检测丢失(速度已置零)", + $"MultiVehicle{CarNum}-stop"); + } + else + { + Hedingben.ToastText("车队检测正常", $"MultiVehicle{CarNum}-stop"); + } + + var rotateHoldForAlignment = false; + if (fleetMode == 2 && fleetReady && !fleetStopActive && !notificationMotionReleased) + { + rotateHoldForAlignment = true; + fleetOmega = 0; + Hedingben.ToastText("车队原地旋转等待主车释放速度", $"MultiVehicle{CarNum}-rotate-align"); + LogMultiVehicleRemoteDecision( + $"ROTATE_HOLD_RELEASE master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"requestedOmega={requestedFleetOmega:0.000}", true); + } + else if (fleetMode == 2 && fleetReady && !fleetStopActive && !rotateFleetWheelsAligned) + { + rotateHoldForAlignment = true; + fleetOmega = 0; + Hedingben.ToastText($"车队原地旋转舵轮预对齐中 car=[{rotateUnalignedCars}]", $"MultiVehicle{CarNum}-rotate-align"); + LogMultiVehicleRemoteDecision( + $"ROTATE_HOLD_ALIGN master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"requestedOmega={requestedFleetOmega:0.000} unalignedCars=[{rotateUnalignedCars}]", true); + } + + if (fleetStopActive) + LogMultiVehicleStop(fleetStopReason); + + VisualizeFleet(contour, layoutX, layoutY, layoutTh); + + // 补偿量提到块外,便于诊断日志统一记录三类来源(检测/SLAM)的贡献。 + float xDetectCompensate = 0, yDetectCompensate = 0, thDetectCompensate = 0; + float xPosCompensate = 0, yPosCompensate = 0, thPosCompensate = 0; + float posBiasX = 0, posBiasY = 0, posBiasTh = 0; + + if (!fleetReady) + LogMultiVehicleRemoteDecision( + $"NO_SEND_NOT_READY master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum} canMove={canMove} mode={fleetMode} " + + $"cmdVx={fleetVx:0.000} cmdFTh={fleetFrontTh:0.00} cmdRTh={fleetRearTh:0.00} omega={fleetOmega:0.000}"); + + if (fleetStopActive) + { + chassis.RampStop(); + SetMultiVehicleMotionFeasible(true); + if (fleetMode == 2) + { + MultiVehicleRotateWheelsReady = false; + MultiVehicleRotateFleetReady = false; + _multiVehicleRotateAlignDetail = $"fleet stop: {fleetStopReason}"; + } + LogMultiVehicleRemoteDecision( + $"RAMP_STOP master={isMaster} manual={manualEnabled} auto={autoEnabled} ready={fleetReady} " + + $"reason={fleetStopReason}", true); + } + + if (fleetReady && !fleetStopActive) + { + chassis.SetOriginBias(layoutX, layoutY, layoutTh); + // E: 统一控制点半径,取 syncDistance/2,与编队几何一致。 + var controlRadius = syncDistance / 2f; + chassis.ControlPointRadius = controlRadius; + // #1 纠偏随旋转缩放:把每轮纠偏钳到旋转切向的比例,减速末段切向变小时纠偏同步缩小,杜绝轮向乱摆。 + chassis.RotateCompTangentFrac = rotateParams.CompTangentFrac; + + if (canMove && Conf.MultiVehicleUseDetect) + { + xDetectCompensate = Math.Abs(detectDx) > Conf.SingleCarSyncPrecisionXy + ? detectDx * Conf.MultiVehicleDetectBiasXFac : 0; + yDetectCompensate = Math.Abs(detectDy) > Conf.SingleCarSyncPrecisionXy + ? detectDy * Conf.MultiVehicleDetectBiasYFac : 0; + thDetectCompensate = Math.Abs(detectDth) > Conf.SingleCarSyncPrecisionTh + ? detectDth * Conf.MultiVehicleDetectBiasThFac : 0; + + xDetectCompensate = ClampBias(xDetectCompensate, Conf.MultiVehicleDetectBiasXThreshold); + yDetectCompensate = ClampBias(yDetectCompensate, Conf.MultiVehicleDetectBiasYThreshold); + thDetectCompensate = ClampBias(thDetectCompensate, Conf.MultiVehicleDetectBiasThThreshold); + } + + // 车队内姿态纠正(POS 补偿):仅当 useDetourCorrection 开启且本车读到全局定位时施加。 + // 这是"定位参与车队内姿态纠正"的唯一开关点——关闭它不影响上面的整队姿态计算/安全门。 + if (canMove && useDetourCorrection && slamRead && TryInferFleetCenter(out _, out _, out _)) + { + var supposedPos = LessMath.Transform2D( + Tuple.Create(CenterX, CenterY, CenterTh), + Tuple.Create(layoutX, layoutY, layoutTh)); + supposedPosForDiag = supposedPos; + var posBias = LessMath.SolveTransform2D(Tuple.Create(selfX, selfY, selfTh), supposedPos); + posBiasX = (float)posBias.Item1; + posBiasY = (float)posBias.Item2; + posBiasTh = (float)posBias.Item3; + + xPosCompensate = Math.Abs(posBias.Item1) > Conf.SingleCarSyncPrecisionXy + ? ClampBias(posBias.Item1 * Conf.MultiVehiclePosBiasXFac, Conf.MultiVehiclePosBiasXThreshold) + : 0; + yPosCompensate = Math.Abs(posBias.Item2) > Conf.SingleCarSyncPrecisionXy + ? ClampBias(posBias.Item2 * Conf.MultiVehiclePosBiasYFac, Conf.MultiVehiclePosBiasYThreshold) + : 0; + thPosCompensate = Math.Abs(posBias.Item3) > Conf.SingleCarSyncPrecisionTh + ? ClampBias(posBias.Item3 * Conf.MultiVehiclePosBiasThFac, Conf.MultiVehiclePosBiasThThreshold) + : 0; + } + + sendMotionPainter.Clear(); + if (fleetMode == 2) + { + // 原地旋转:底盘原点已偏置到车队中心,绕队中心旋转,并叠加 PI 闭环纠偏维持队形。 + // detect/pos 偏差(dx,dy,dth) 本身就是"本车参考点应在车体系移动到的位置/应转的角",作为误差喂给 PI。 + // 纯 P 对抗恒定横向滑移有稳态残差,积分项消除之。检测丢失(canMove=false)时不纠偏并清空积分。 + float rotCompVx = 0, rotCompVy = 0, rotCompOmega = 0; + var now = DateTime.Now; + // dt 限幅,避免首帧/卡顿导致积分突跳 + var dt = _rotPiLastTime == DateTime.MinValue ? 0f + : (float)Math.Min(0.2, Math.Max(0, (now - _rotPiLastTime).TotalSeconds)); + _rotPiLastTime = now; + + // 仅在"被指令旋转"时才纠偏:松开摇杆(fleetOmega≈0)时绝不再下发补偿, + // 否则积分残留会持续驱动车辆平移/旋转,表现为"松杆后轮子来回打、自转停不下来"。 + var rotating = Math.Abs(fleetOmega) > rotateParams.ActiveOmega; + if (canMove && rotating) + { + // #3 抗饱和:上一拍舵轮未对齐(gate=0、车没真正转动)时冻结积分,避免卡死时积分越积越大。 + var allowInteg = chassis.LastRotateAligned && + MultiVehicleRotateWheelsReady && + MultiVehicleRotateFleetReady && + !rotateHoldForAlignment; + var errX = detectDx + posBiasX; // mm,车体系:本车纵向(前+)应移动量 + var errY = detectDy + posBiasY; // mm,车体系:本车横向(左+)应移动量 + var errTh = detectDth + posBiasTh; // deg,本车应转角 + rotCompVx = RotatePiTerm(errX, ref _rotIntegX, Conf.SingleCarSyncPrecisionXy, + rotateParams.CompXyFac, rotateParams.CompXyIFac, + rotateParams.CompXyMax, dt, allowInteg); + rotCompVy = RotatePiTerm(errY, ref _rotIntegY, Conf.SingleCarSyncPrecisionXy, + rotateParams.CompXyFac, rotateParams.CompXyIFac, + rotateParams.CompXyMax, dt, allowInteg); + rotCompOmega = RotatePiTerm(errTh, ref _rotIntegTh, Conf.SingleCarSyncPrecisionTh, + rotateParams.CompThFac, rotateParams.CompThIFac, + rotateParams.CompThMax, dt, allowInteg); + + // #1 纠偏随旋转指令缩放:comp ×= |fleetOmega| / 本次峰值。 + // 加速+匀速段峰值≈当前 → 系数≈1(全力纠偏,不削弱);减速段当前<峰值 → 系数随转速同步下降。 + // 关键:旋转切向也∝转速,故"纠偏:切向"比例全程恒定=匀速段比例(远<1),既杜绝末段轮子乱打方向, + // 又不像按切向钳位那样在匀速段就削弱纠偏。 + var absOmega = Math.Abs(fleetOmega); + _rotOmegaPeak = Math.Max(_rotOmegaPeak, absOmega); + if (_rotOmegaPeak > 1e-3f) + { + var compScale = Math.Min(1f, absOmega / _rotOmegaPeak); + rotCompVx *= compScale; + rotCompVy *= compScale; + rotCompOmega *= compScale; + } + LimitRotateCompensation(ref rotCompVx, ref rotCompVy, ref rotCompOmega, + absOmega, controlRadius, rotateParams.CompTangentFrac); + } + else + { + // 检测丢失或未指令旋转:清零补偿并复位积分/计时/峰值,停止时不再有残留驱动。 + _rotIntegX = _rotIntegY = _rotIntegTh = 0; + _rotPiLastTime = DateTime.MinValue; + _rotOmegaPeak = 0; + } + + _mvRotCompVx = rotCompVx; + _mvRotCompVy = rotCompVy; + _mvRotCompOmega = rotCompOmega; + + bool motionOk; + if (rotateHoldForAlignment) + { + motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams, + rotCompVx, rotCompVy, rotCompOmega); + } + else if (rotating) + { + // Preparation calls SendRotateMotion with zero delta; after release it would starve the speed ramp. + if (!MultiVehicleRotateWheelsReady) + { + motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams, + rotCompVx, rotCompVy, rotCompOmega, rampStop: false); + rotateHoldForAlignment = true; + fleetOmega = 0; + chassis.RampStop(); + if (!motionOk || !MultiVehicleRotateWheelsReady) + { + LogMultiVehicleRemoteDecision( + $"ROTATE_HOLD_LOCAL_ALIGN master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"requestedOmega={requestedFleetOmega:0.000} align={_multiVehicleRotateAlignDetail}", true); + } + else + { + LogMultiVehicleRemoteDecision( + $"ROTATE_HOLD_LOCAL_READY master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"requestedOmega={requestedFleetOmega:0.000} align={_multiVehicleRotateAlignDetail}", true); + } + } + else + { + motionOk = chassis.SendRotateMotion(fleetOmega, + localCompensateX: rotCompVx, localCompensateY: rotCompVy, + localCompensateTh: rotCompOmega); + var activeWheelAlignDeg = rotateParams.ActiveWheelAlignDeg; + var activeAligned = TryCheckRotateWheelAlignment(chassis, + activeWheelAlignDeg, out _multiVehicleRotateAlignDetail); + if (!motionOk) + MultiVehicleRotateWheelsReady = false; + if (motionOk && !activeAligned) + { + MultiVehicleRotateWheelsReady = false; + MultiVehicleRotateFleetReady = false; + rotateHoldForAlignment = true; + fleetOmega = 0; + _rotIntegX = _rotIntegY = _rotIntegTh = 0; + _rotPiLastTime = DateTime.MinValue; + _rotOmegaPeak = 0; + rotCompVx = rotCompVy = rotCompOmega = 0; + _mvRotCompVx = _mvRotCompVy = _mvRotCompOmega = 0; + motionOk = PrepareMultiVehicleRotateWheels(chassis, requestedFleetOmega, rotateParams); + LogMultiVehicleRemoteDecision( + $"ROTATE_ACTIVE_REHOLD master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"requestedOmega={requestedFleetOmega:0.000} activeTol={activeWheelAlignDeg:0.0} " + + $"align={_multiVehicleRotateAlignDetail}", true); + } + } + } + else + { + motionOk = chassis.SendRotateMotion(fleetOmega, + localCompensateX: rotCompVx, localCompensateY: rotCompVy, + localCompensateTh: rotCompOmega); + MultiVehicleRotateWheelsReady = motionOk && + TryCheckRotateWheelAlignment(chassis, rotateParams.StartWheelAlignDeg, + out _multiVehicleRotateAlignDetail); + } + SetMultiVehicleMotionFeasible(motionOk, motionOk ? "" : chassis.LastMotionDecomposeFailureReason); + if (!motionOk) + { + AddFleetStop($"car{CarNum} chassis rotate infeasible: {chassis.LastMotionDecomposeFailureReason}", + CarNum); + canMove = false; + LogMultiVehicleStop(fleetStopReason, true); + } + LogRotateWheelOutputs(chassis, rotateHoldForAlignment ? "hold-align" : (rotating ? "rotate" : "idle"), + fleetOmega); + if (fleetPosValid) + LogMultiVehicleRotateCenterDrift(isMaster, manualEnabled, autoEnabled, fleetReady, canMove, rotating, + CenterX, CenterY, CenterTh, requestedFleetOmega, fleetOmega, + rotCompVx, rotCompVy, rotCompOmega); + else + ResetMultiVehicleRotateCenterDrift(); + LogMultiVehicleRemoteDecision( + $"SEND_ROTATE master={isMaster} manual={manualEnabled} canMove={canMove} ready={fleetReady} ok={motionOk} " + + $"holdAlign={rotateHoldForAlignment} wheelReady={MultiVehicleRotateWheelsReady} fleetReady={MultiVehicleRotateFleetReady} " + + $"mode={fleetMode} omegaReq={requestedFleetOmega:0.000} omega={fleetOmega:0.000} " + + $"comp=({rotCompVx:0.0},{rotCompVy:0.0},{rotCompOmega:0.000}) align={_multiVehicleRotateAlignDetail} " + + $"rotParam(xyP={rotateParams.CompXyFac:0.###} xyI={rotateParams.CompXyIFac:0.###} " + + $"xyMax={rotateParams.CompXyMax:0.#} thP={rotateParams.CompThFac:0.###} " + + $"thI={rotateParams.CompThIFac:0.###} thMax={rotateParams.CompThMax:0.#} " + + $"frac={rotateParams.CompTangentFrac:0.###} activeOmega={rotateParams.ActiveOmega:0.###} " + + $"startTol={rotateParams.StartWheelAlignDeg:0.#} activeTol={rotateParams.ActiveWheelAlignDeg:0.#}) " + + $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); + + // 仅主车:读取两车实际 sim 位姿,量化"开环横向滑移"来源(节流 ~200ms)。 + if (isMaster && Conf.MultiVehicleRotatePoseWebApiDiagEnabled) + LogRotatePoseSample(); + } + else + { + _mvRotCompVx = 0; + _mvRotCompVy = 0; + _mvRotCompOmega = 0; + MultiVehicleRotateFleetReady = true; + // Reset rotate-only PI state outside rotate mode. + _rotIntegX = _rotIntegY = _rotIntegTh = 0; + _rotPiLastTime = DateTime.MinValue; + _rotPoseEpisode = false; + _rotPosePrevTime = DateTime.MinValue; + ResetMultiVehicleRotateCenterDrift(); + // 常规/蟹行:蟹行时 frontTh==rearTh(四轮同向)即为平移,与常规共用同一下发路径。 + var motionOk = chassis.SendMotion(fleetVx, fleetFrontTh, fleetRearTh, localControlRadius: controlRadius, + localCompensateX: xDetectCompensate + xPosCompensate, + localCompensateY: yDetectCompensate + yPosCompensate, + localCompensateTh: thDetectCompensate + thPosCompensate); + if (motionOk) + MultiVehicleRotateWheelsReady = TryCheckRotateWheelAlignment(chassis, + Math.Max(0.1f, Conf.FleetCrabStartWheelAlignDeg), out _multiVehicleRotateAlignDetail); + else + { + MultiVehicleRotateWheelsReady = false; + _multiVehicleRotateAlignDetail = chassis.LastMotionDecomposeFailureReason; + } + MultiVehicleRotateFleetReady = true; + SetMultiVehicleMotionFeasible(motionOk, motionOk ? "" : chassis.LastMotionDecomposeFailureReason); + if (!motionOk) + { + AddFleetStop($"car{CarNum} chassis motion infeasible: {chassis.LastMotionDecomposeFailureReason}", + CarNum); + canMove = false; + LogMultiVehicleStop(fleetStopReason, true); + } + LogMultiVehicleRemoteDecision( + $"SEND_MOTION master={isMaster} manual={manualEnabled} canMove={canMove} ready={fleetReady} ok={motionOk} " + + $"mode={fleetMode} vx={fleetVx:0.000} fTh={fleetFrontTh:0.00} rTh={fleetRearTh:0.00} " + + $"comp=({xDetectCompensate + xPosCompensate:0.0},{yDetectCompensate + yPosCompensate:0.0},{thDetectCompensate + thPosCompensate:0.000}) " + + $"wheelReady={MultiVehicleRotateWheelsReady} align={_multiVehicleRotateAlignDetail} " + + $"fleetCnt={fleetCount}/{Conf.MultiVehicleFleetNum}"); + } + } + + // === 诊断日志(节流 ~300ms)=== + // 用于定位“剧烈运动”来源:BASE=遥控基础速度;DETECT=2腿检测补偿;POS=SLAM编队补偿;SEND=最终下发。 + // 若 BASE≈0 但 SEND 持续非零,说明是补偿在驱动;再看 DETECT/POS 哪一路的 comp 持续非零即定位到来源。 + // 同时落 DLog(tag MultiVehicleDbg) 与屏幕 ToastText(tag MultiVehicle{CarNum}-dbg),后者无需开启磁盘转储即可直接观察。 + if ((DateTime.Now - _mvDbgLastLog).TotalMilliseconds >= 300) + { + _mvDbgLastLog = DateTime.Now; + var spacingStr = float.IsNaN(currentSpacing) ? "NaN" : currentSpacing.ToString("F0"); + var cx = xDetectCompensate + xPosCompensate; + var cy = yDetectCompensate + yPosCompensate; + var cth = thDetectCompensate + thPosCompensate; + var actualCenterThForDiag = inferredFleetCenterValid ? inferredFleetCenterTh : CenterTh; + var idealHeadingErr = (float)CommonMath.ThDiff(MultiVehicleAutoIdealTh, actualCenterThForDiag); + var idealHeadingErrReverse = (float)CommonMath.ThDiff(actualCenterThForDiag, MultiVehicleAutoIdealTh); + var frontRearDiff = (float)CommonMath.ThDiff(fleetFrontTh, fleetRearTh); + var commandHeadingSplit = frontRearDiff / 2f; + var commandBaseTh = (float)CommonMath.RoundTh(fleetRearTh + commandHeadingSplit); + var supposedPosText = supposedPosForDiag == null + ? "N/A" + : $"({supposedPosForDiag.Item1:0},{supposedPosForDiag.Item2:0},{supposedPosForDiag.Item3:0.0})"; + var crabDbg = isMaster && manualEnabled && fleetMode == 1 + ? $"| CRAB speed:{crabInputVx:F3} steerRatio:{crabInputVy:F3} raw:{crabRawAngle:F2} limit:{crabSteerLimit:F1} rev:{crabReverseEquivalent} " + : ""; + + var dbg = + $"car{CarNum} master:{isMaster} manual:{manualEnabled} auto:{autoEnabled} slam:{slamRead} corr:{useDetourCorrection} fleetPos:{fleetPosValid} " + + $"ready:{fleetReady}({fleetCount}/{Conf.MultiVehicleFleetNum}) canMove:{canMove} stop:{fleetStopActive} stopReason:{fleetStopReason} useDetect:{Conf.MultiVehicleUseDetect} " + + $"| BASE vx:{fleetVx:F3} fTh:{fleetFrontTh:F2} rTh:{fleetRearTh:F2} " + + crabDbg + + $"| DETECT valid:{detectValid} center({_mvLastDetCenterX:F0},{_mvLastDetCenterY:F0}) dir:{_mvLastDetDir:F1} ndist:{_mvLastDetDist:F0} " + + $"dx:{detectDx:F0} dy:{detectDy:F0} dth:{detectDth:F2} spacing:{spacingStr}/{syncDistance:F0} delta:{deltaDetectCenter:F0} guessX:{Conf.TwoLegGuessX:F0} outBias({Conf.TwoLegOutputBiasX:F0},{Conf.TwoLegOutputBiasY:F0}) " + + $"-> comp x:{xDetectCompensate:F1} y:{yDetectCompensate:F1} th:{thDetectCompensate:F2} " + + $"fac({Conf.MultiVehicleDetectBiasXFac:F2},{Conf.MultiVehicleDetectBiasYFac:F2},{Conf.MultiVehicleDetectBiasThFac:F2}) " + + $"lim({Conf.MultiVehicleDetectBiasXThreshold:F1},{Conf.MultiVehicleDetectBiasYThreshold:F1},{Conf.MultiVehicleDetectBiasThThreshold:F1}) " + + $"| POS self({selfX:F0},{selfY:F0},{selfTh:F1}) center({CenterX:F0},{CenterY:F0},{CenterTh:F1}) " + + $"bias({posBiasX:F0},{posBiasY:F0},{posBiasTh:F1}) -> comp x:{xPosCompensate:F1} y:{yPosCompensate:F1} th:{thPosCompensate:F2} " + + $"fac({Conf.MultiVehiclePosBiasXFac:F2},{Conf.MultiVehiclePosBiasYFac:F2},{Conf.MultiVehiclePosBiasThFac:F2}) " + + $"lim({Conf.MultiVehiclePosBiasXThreshold:F1},{Conf.MultiVehiclePosBiasYThreshold:F1},{Conf.MultiVehiclePosBiasThThreshold:F1}) " + + $"| LAYOUT({layoutX:F0},{layoutY:F0},{layoutTh:F0}) R:{syncDistance / 2f:F0} " + + $"| SEND mode:{fleetMode} omega:{fleetOmega:F1} vx:{fleetVx:F3} fTh:{fleetFrontTh:F2} rTh:{fleetRearTh:F2} cx:{cx:F1} cy:{cy:F1} cth:{cth:F2} " + + $"rotComp(vx:{_mvRotCompVx:F1} vy:{_mvRotCompVy:F1} om:{_mvRotCompOmega:F2}) " + + $"| ROT_ALIGN hold:{rotateHoldForAlignment} wheelReady:{MultiVehicleRotateWheelsReady} fleetReady:{MultiVehicleRotateFleetReady} " + + $"unaligned:[{rotateUnalignedCars}] reqOmega:{requestedFleetOmega:F1} detail:{_multiVehicleRotateAlignDetail}"; + + DLog.Log(dbg, "MultiVehicleDbg"); + FleetDiag(dbg); + + if (autoMode || autoEnabled || MultiVehicleAutoEnabled) + DLog.Log( + $"APPLY car{CarNum} master:{isMaster} autoMode:{autoMode} manual:{manualEnabled} " + + $"cmd(vx:{fleetVx:0.000},f:{fleetFrontTh:0.00},r:{fleetRearTh:0.00},base:{commandBaseTh:0.00},split:{commandHeadingSplit:0.00},f-r:{frontRearDiff:0.00}) " + + $"ideal(has:{MultiVehicleAutoHasIdeal},x:{MultiVehicleAutoIdealX:0},y:{MultiVehicleAutoIdealY:0},th:{MultiVehicleAutoIdealTh:0.00}) " + + $"centerPublished({CenterX:0},{CenterY:0},{CenterTh:0.00}) " + + $"centerInferred(valid:{inferredFleetCenterValid},x:{inferredFleetCenterX:0},y:{inferredFleetCenterY:0},th:{inferredFleetCenterTh:0.00}) " + + $"idealHeadingErr(target-actual):{idealHeadingErr:0.00} reverse(actual-target):{idealHeadingErrReverse:0.00} " + + $"layout({layoutX:0},{layoutY:0},{layoutTh:0.00}) self({selfX:0},{selfY:0},{selfTh:0.00}) supposed:{supposedPosText} " + + $"detectDth:{detectDth:0.00}->comp:{thDetectCompensate:0.00} " + + $"posBiasTh:{posBiasTh:0.00}->comp:{thPosCompensate:0.00} " + + $"totalLocalComp(x:{cx:0.0},y:{cy:0.0},th:{cth:0.00}) " + + $"useDetourCorr:{useDetourCorrection} slam:{slamRead} fleetPos:{fleetPosValid} ready:{fleetReady}", + "FleetCrabHeadingDbg"); + + // 屏幕分两行显示,便于直接观察(无需开启 DLog 磁盘转储) + Hedingben.ToastText( + $"BASE vx{fleetVx:F3} fTh{fleetFrontTh:F1} | SEND vx{fleetVx:F3} c({cx:F0},{cy:F0},{cth:F1}) " + + $"| ready{fleetReady} canMove{canMove} stop{fleetStopActive}", + $"MultiVehicle{CarNum}-dbg"); + Hedingben.ToastText( + $"DET v{detectValid} dx{detectDx:F0} dy{detectDy:F0} dth{detectDth:F1} cmp({xDetectCompensate:F0},{yDetectCompensate:F0},{thDetectCompensate:F1}) " + + $"| POS bias({posBiasX:F0},{posBiasY:F0},{posBiasTh:F1}) cmp({xPosCompensate:F0},{yPosCompensate:F0},{thPosCompensate:F1})", + $"MultiVehicle{CarNum}-dbg2"); + } + + if (isMaster) + { + lock (FleetLock) + { + MultiVehicleFleet[CarNum] = BuildSelfInfo(true, slamRead, selfX, selfY, selfTh, + layoutX, layoutY, layoutTh, selfAligned, ownDetectOk); + } + + if (fleetReady || fleetStopActive) + { + VehicleSyncNotification notification; + lock (FleetLock) + { + notification = new VehicleSyncNotification + { + // 广播"整队姿态有效":主车读到全局定位即为真,从车据此放行自动模式安全门。 + PosAvailable = slamRead, + CenterX = CenterX, + CenterY = CenterY, + CenterTh = CenterTh, + Aligned = MultiVehicleAligned, + Fleet = new Dictionary(MultiVehicleFleet), + FleetVx = fleetStopActive ? 0 : fleetVx, + FleetFrontTh = fleetStopActive ? 0 : fleetFrontTh, + FleetRearTh = fleetStopActive ? 0 : fleetRearTh, + Mode = fleetMode, + FleetOmega = fleetStopActive ? 0 : fleetOmega, + RequestedFleetOmega = fleetMode == 2 && !fleetStopActive ? requestedFleetOmega : 0, + FleetMotionReleased = !fleetStopActive && !(fleetMode == 2 && rotateHoldForAlignment), + FleetStopActive = fleetStopActive, + FleetStopReason = fleetStopReason, + FleetStopSourceCar = fleetStopSourceCar, + AutoEnabled = MultiVehicleAutoEnabled, + // 脚本驱动等价于手动联动,广播为 ManualEnabled 让从车解锁跟随。 + ManualEnabled = manualEnabled, + UseDetourCorrection = useDetourCorrection, + SyncTh = syncTh, + SyncDistance = syncDistance, + DeltaDetectCenter = deltaDetectCenter, + // F: 单调递增序列号(从 1 起),从车据此丢弃乱序旧包。 + Seq = ++_multiVehicleNotifySeq, + // D: 透传理想车队中心(仅自动模式且控制器给出时有效)。 + HasIdeal = !manualEnabled && MultiVehicleAutoEnabled && + Conf.MultiVehicleAutoUseIdealCenter && MultiVehicleAutoHasIdeal, + IdealX = MultiVehicleAutoIdealX, + IdealY = MultiVehicleAutoIdealY, + IdealTh = MultiVehicleAutoIdealTh + }; + FillRotateNotificationParams(notification, rotateParams); + } + + LogMultiVehicleRemoteDecision( + $"NOTIFY_SEND seq={notification.Seq} mode={notification.Mode} " + + $"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " + + $"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " + + $"reason={notification.FleetStopReason} useDetourCorr={notification.UseDetourCorrection} " + + $"fleetCnt={notification.Fleet.Count}/{Conf.MultiVehicleFleetNum} " + + $"rotParam(valid={notification.RotateParamsValid} xyP={notification.RotateCompXyFac:0.###} " + + $"xyI={notification.RotateCompXyIFac:0.###} xyMax={notification.RotateCompXyMax:0.#} " + + $"thP={notification.RotateCompThFac:0.###} thI={notification.RotateCompThIFac:0.###} " + + $"thMax={notification.RotateCompThMax:0.#} frac={notification.RotateCompTangentFrac:0.###} " + + $"startTol={notification.RotateStartWheelAlignDeg:0.#} activeTol={notification.RotateActiveWheelAlignDeg:0.#})"); + + foreach (var kv in notification.Fleet) + { + var ip = kv.Value.Ip; + var port = kv.Value.Port > 0 ? kv.Value.Port : WebAPI.port; + if (string.IsNullOrWhiteSpace(ip) || kv.Key == CarNum) + continue; + FireAndForgetNotify(hc, ip, port, notification); + } + } + } + else + { + FireAndForgetRegister(hc, BuildSelfInfo(false, slamRead, selfX, selfY, selfTh, + layoutX, layoutY, layoutTh, selfAligned, ownDetectOk)); + } + } + + private bool IsMultiVehicleMaster() => Conf.MultiVehicleMasterEndpoint == "/"; + + /// + /// C: 剔除超过 TTL 未刷新(register/notify)的 fleet 成员。调用方须已持有 FleetLock。 + /// 从车崩溃/断网后其条目过期被删除,使 fleetReady(数量==总数) 同时蕴含"全部成员在线且新鲜", + /// 主车不再基于过期 layout/DetectOk 继续 SendMotion+notify。 + /// + private void PruneStaleFleetMembers() + { + var ttlMs = Conf.MultiVehicleMemberTtlMs > 0 + ? Conf.MultiVehicleMemberTtlMs + : Math.Max(500, Conf.MultiVehicleSyncInterval * 6); + var now = DateTime.Now; + var stale = MultiVehicleFleet.Keys + .Where(k => k != CarNum && + (!_multiVehicleFleetSeen.TryGetValue(k, out var seen) || + (now - seen).TotalMilliseconds > ttlMs)) + .ToList(); + foreach (var k in stale) + { + MultiVehicleFleet.Remove(k); + _multiVehicleFleetSeen.Remove(k); + Hedingben.ToastText($"剔除掉线成员 car{k}({ttlMs:F0}ms 未刷新)", $"MultiVehicle{CarNum}-prune"); + } + } + + private bool FleetHasPosAvailable() + { + lock (FleetLock) + return MultiVehicleFleet.Values.Any(v => v.PosAvailable); + } + + // 基于 SLAM 世界坐标的最近邻车间距(mm);无可用定位邻车时返回 NaN。 + private float TryGetSlamSpacing(float selfX, float selfY) + { + List others; + lock (FleetLock) + others = MultiVehicleFleet + .Where(kv => kv.Key != CarNum && kv.Value.PosAvailable) + .Select(kv => kv.Value).ToList(); + + if (others.Count == 0) return float.NaN; + return others.Min(o => (float)Math.Sqrt((o.X - selfX) * (o.X - selfX) + (o.Y - selfY) * (o.Y - selfY))); + } + + private bool TryInferFleetCenter(out float centerX, out float centerY, out float centerTh) + { + centerX = centerY = centerTh = 0; + List positioned; + lock (FleetLock) + positioned = MultiVehicleFleet.Values.Where(v => v.PosAvailable).ToList(); + + if (positioned.Count == 0) return false; + + var centers = positioned.Select(InferFleetCenterFromCar).ToList(); + centerX = centers.Average(c => c.Item1); + centerY = centers.Average(c => c.Item2); + var avgSin = centers.Average(c => Math.Sin(c.Item3 * Math.PI / 180.0)); + var avgCos = centers.Average(c => Math.Cos(c.Item3 * Math.PI / 180.0)); + centerTh = (float)(Math.Atan2(avgSin, avgCos) * 180.0 / Math.PI); + return true; + } + + public bool TryGetFleetCenterFromMembers(out float centerX, out float centerY, out float centerTh) + { + return TryInferFleetCenter(out centerX, out centerY, out centerTh); + } + + private static (float, float, float) InferFleetCenterFromCar(VehicleSyncInfo car) + { + // 由 carWorld = Transform2D(center, layout) 反推 center = carWorld ∘ layout⁻¹。 + // 注意 layout⁻¹ 是真正的 SE(2) 逆,而非逐分量取负 (-layout): + // 当 layoutTh≠0(如从车 180°)时,逐分量取负会把平移方向算错,导致车队中心偏移 → 位姿补偿跑飞。 + var layout = Tuple.Create((double)car.LayoutX, (double)car.LayoutY, (double)car.LayoutTh); + var layoutInv = LessMath.SolveTransform2D(layout, Tuple.Create(0.0, 0.0, 0.0)); + var center = LessMath.Transform2D( + Tuple.Create((double)car.X, (double)car.Y, (double)car.Th), + layoutInv); + return ((float)center.Item1, (float)center.Item2, (float)center.Item3); + } + + /// + /// 由本车(通常为主车)当前 Detour SLAM 位姿反推车队中心位姿(世界系)。 + /// 复用 GetLayoutPose + InferFleetCenterFromCar 的同款 SE(2) 反推(center = carWorld ∘ layout⁻¹), + /// 保证与自动模式车队中心计算一致。供动作(如 FleetCrabWalk)取路径起点用;无有效定位时返回 false。 + /// + public bool TryGetFleetCenterFromSlam(out float centerX, out float centerY, out float centerTh) + { + centerX = centerY = centerTh = 0; + var carPos = DetourInterface.getCartLocation(); + return TryGetFleetCenterFromPose((float)carPos.x, (float)carPos.y, (float)carPos.th, + out centerX, out centerY, out centerTh); + } + + public bool TryGetFleetCenterFromPose(float carX, float carY, float carTh, + out float centerX, out float centerY, out float centerTh) + { + centerX = centerY = centerTh = 0; + var (layoutX, layoutY, layoutTh) = GetLayoutPose(Conf.TestCarSyncTh, Conf.TestCarSyncDistance); + var self = new VehicleSyncInfo + { + X = carX, + Y = carY, + Th = carTh, + LayoutX = layoutX, + LayoutY = layoutY, + LayoutTh = layoutTh + }; + var (cx, cy, cth) = InferFleetCenterFromCar(self); + centerX = cx; + centerY = cy; + centerTh = cth; + return true; + } + + /// + /// 预热:用主车 SLAM 位姿,立即(1) 对外发布有效"车队中心快照",(2) 把主车自身以 posAvailable=true + /// 写入 fleet 表。供 FleetCrabWalk 等动作在启动控制器 Track() 前调用,解决两个启动期问题: + /// - 控制器首帧通过 MultiVehicleGetFleetPos 读到 (0,0,0) → 误判已到终点、立即结束; + /// - 自动联动循环的安全门 FleetHasPosAvailable(PilotDefinition.cs ~402)在冷启动(无车上报定位)时 + /// 会把 AutoEnabled 关掉、循环无法发布真实中心。播种主车 posAvailable 条目即可越过该门,使循环正常接管。 + /// 需在主车上调用;getCartLocation() 无定位时会阻塞(与自动模式一致)。 + /// + public bool PrimeMasterAutoFromSlam() + { + var carPos = DetourInterface.getCartLocation(); + var (lx, ly, lth) = GetLayoutPose(Conf.TestCarSyncTh, Conf.TestCarSyncDistance); + var info = BuildSelfInfo(true, true, (float)carPos.x, (float)carPos.y, (float)carPos.th, + lx, ly, lth, false); + var (cx, cy, cth) = InferFleetCenterFromCar(info); + PublishFleetCenter(cx, cy, cth); + lock (FleetLock) + { + MultiVehicleFleet[CarNum] = info; + _multiVehicleFleetSeen[CarNum] = DateTime.Now; + } + return true; + } + + public long BeginFleetMotionWarmup() + { + MultiVehicleRotateWheelsReady = false; + MultiVehicleRotateFleetReady = false; + _multiVehicleRotateAlignDetail = "fleet motion warmup pending"; + return Interlocked.Read(ref _multiVehicleNotifySeq); + } + + public bool IsFleetMotionWarmupReady(DateTime warmStartTime, long notificationSeqBaseline, + float syncTh, float syncDistance, out string detail) + { + var pending = new List(); + var requiredCount = Math.Max(1, Conf.MultiVehicleFleetNum); + lock (FleetLock) + { + if (MultiVehicleFleet.Count != requiredCount) + pending.Add($"fleetCnt={MultiVehicleFleet.Count}/{requiredCount}"); + + foreach (var kv in MultiVehicleFleet.OrderBy(k => k.Key)) + { + var carNum = kv.Key; + var info = kv.Value; + if (!_multiVehicleFleetSeen.TryGetValue(carNum, out var seen) || seen < warmStartTime) + pending.Add($"car{carNum}:notFresh"); + + var (layoutX, layoutY, layoutTh) = GetLayoutPoseForCar(carNum, syncTh, syncDistance); + if (Math.Abs(info.LayoutX - layoutX) > 1f || + Math.Abs(info.LayoutY - layoutY) > 1f || + Math.Abs(CommonMath.ThDiff(info.LayoutTh, layoutTh)) > 1f) + pending.Add( + $"car{carNum}:layout=({info.LayoutX:0},{info.LayoutY:0},{info.LayoutTh:0.0})"); + + if (!info.MotionFeasible) + pending.Add($"car{carNum}:motion={info.MotionInfeasibleReason}"); + if (!info.RotateWheelsAligned) + pending.Add($"car{carNum}:wheel={info.RotateWheelAlignDetail}"); + if (carNum != CarNum && info.AppliedNotificationSeq <= notificationSeqBaseline) + pending.Add($"car{carNum}:seq={info.AppliedNotificationSeq}<={notificationSeqBaseline}"); + } + } + + detail = pending.Count == 0 + ? $"ready seqBase={notificationSeqBaseline}" + : string.Join("; ", pending); + return pending.Count == 0; + } + + private (float, float, float) GetLayoutPose(float syncTh, float syncDistance) + => GetLayoutPoseForCar(CarNum, syncTh, syncDistance); + + private static (float, float, float) GetLayoutPoseForCar(int carNum, float syncTh, float syncDistance) + { + // Two-car layout: members are mirrored around the fleet center; car2 faces 180 deg away. + var rad = syncTh / 180f * Math.PI; + var sign = carNum == 1 ? 1f : -1f; + var xx = (float)(Math.Cos(rad) * syncDistance / 2 * sign); + var yy = (float)(Math.Sin(rad) * syncDistance / 2 * sign); + return (xx, yy, syncTh + (carNum == 1 ? 0 : 180)); + } + + private VehicleSyncInfo BuildSelfInfo(bool master, bool posAvailable, float x, float y, float th, + float layoutX, float layoutY, float layoutTh, bool aligned, bool detectOk = true) + { + ResolveMultiVehicleSelfEndpoint(out var selfIp, out var selfPort); + + return new VehicleSyncInfo + { + Master = master, + Ip = selfIp, + Port = selfPort, + PosAvailable = posAvailable, + X = x, + Y = y, + Th = th, + LayoutX = layoutX, + LayoutY = layoutY, + LayoutTh = layoutTh, + Aligned = aligned, + DetectOk = detectOk, + MotionFeasible = _multiVehicleMotionFeasible, + MotionInfeasibleReason = _multiVehicleMotionInfeasibleReason, + RotateWheelsAligned = MultiVehicleRotateWheelsReady, + RotateWheelAlignDetail = _multiVehicleRotateAlignDetail, + AppliedNotificationSeq = _multiVehicleAppliedSeq + }; + } + + private void VisualizeFleet(List contour, float egoLayoutX, float egoLayoutY, float egoLayoutTh) + { + // 画在本车车体系:global=false,渲染时由 ClumsyLite 叠加本车真实位姿。 + // 每个成员按“相对本车 layout 的位姿”绘制(memberInEgo = egoLayout⁻¹ ∘ memberLayout), + // 这样手动模式无 SLAM 定位也能正确显示编队相对关系;避免之前用车队系 layout 坐标叠加本车位姿造成的整体平移。 + var fleetPainter = UI.GetPainter("MultiVehicleFleet-vis", false); + Dictionary fleet; + lock (FleetLock) + fleet = new Dictionary(MultiVehicleFleet); + + var egoLayout = Tuple.Create((double)egoLayoutX, (double)egoLayoutY, (double)egoLayoutTh); + + Tuple MemberInEgo(float mx, float my, float mth) + => LessMath.SolveTransform2D(egoLayout, Tuple.Create((double)mx, (double)my, (double)mth)); + + void DrawContour(Color color, Tuple rel) + { + var transformed = contour + .Select(pp => LessMath.Transform2D((float)rel.Item1, (float)rel.Item2, (float)rel.Item3, pp)) + .ToList(); + for (var i = 0; i < 4; ++i) + fleetPainter.DrawLine(color, transformed[i], transformed[(i + 1) % 4]); + // 对角线便于辨认朝向 + fleetPainter.DrawLine(color, transformed[0], transformed[2]); + fleetPainter.DrawLine(color, transformed[1], transformed[3]); + } + + fleetPainter.Clear(); + foreach (var kvp in fleet) + { + var color = kvp.Value.Master ? Color.Red : Color.Orange; + var rel = MemberInEgo(kvp.Value.LayoutX, kvp.Value.LayoutY, kvp.Value.LayoutTh); + DrawContour(color, rel); + fleetPainter.DrawText(color, $"{kvp.Key}", new Vector((float)rel.Item1, (float)rel.Item2)); + } + } + + private void FireAndForgetRegister(HttpClient hc, VehicleSyncInfo info) + { + try + { + ParseMasterEndpoint(out var masterIp, out var masterPort); + var payload = VehicleSyncBinaryCodec.EncodeRegister(CarNum, info); + var url = $"http://{masterIp}:{masterPort}/multi-vehicle-register-bin"; + _ = PostBinaryAsync(hc, url, payload, "register"); + } + catch (Exception e) + { + DLog.Log($"register binary encode failed: {e.GetBaseException().Message}", "MultiVehicle"); + } + } + + private void FireAndForgetNotify(HttpClient hc, string ip, int port, VehicleSyncNotification notification) + { + try + { + var payload = VehicleSyncBinaryCodec.EncodeNotification(notification); + var url = $"http://{ip}:{port}/multi-vehicle-notify-bin"; + _ = PostBinaryAsync(hc, url, payload, $"notify {ip}:{port}"); + } + catch (Exception e) + { + DLog.Log($"notify {ip}:{port} binary encode failed: {e.GetBaseException().Message}", "MultiVehicle"); + } + } + + private static async Task PostBinaryAsync(HttpClient hc, string url, byte[] payload, string description) + { + try + { + using var content = new ByteArrayContent(payload); + content.Headers.ContentType = + new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + using var response = await hc.PostAsync(url, content).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + return; + + DLog.Log( + $"{description} binary failed: HTTP {(int)response.StatusCode} {response.ReasonPhrase}", + "MultiVehicle"); + } + catch (Exception e) + { + DLog.Log($"{description} binary failed: {e.GetBaseException().Message}", "MultiVehicle"); + } + } + + private void ResolveMultiVehicleSelfEndpoint(out string ip, out int port) + { + ip = "127.0.0.1"; + port = WebAPI.port > 0 ? WebAPI.port : 8008; + + var endpoint = Conf.MultiVehicleSelfEndpoint; + if (string.IsNullOrWhiteSpace(endpoint)) return; + + var parts = endpoint.Trim().Split(':'); + if (parts.Length >= 1 && !string.IsNullOrWhiteSpace(parts[0])) + ip = parts[0].Trim(); + if (parts.Length >= 2 && int.TryParse(parts[1], out var p) && p > 0) + port = p; + } + + private void ParseMasterEndpoint(out string ip, out int port) + { + ip = "127.0.0.1"; + port = 8008; + var endpoint = Conf.MultiVehicleMasterEndpoint ?? "/"; + if (endpoint == "/") return; + var parts = endpoint.Split(':'); + if (parts.Length >= 1 && !string.IsNullOrWhiteSpace(parts[0])) + ip = parts[0]; + if (parts.Length >= 2 && int.TryParse(parts[1], out var p)) + port = p; + } + + private static float ClampBias(float value, float threshold) + => Math.Sign(value) * Math.Min(Math.Abs(value), threshold); + + private RotateControlParams BuildRotateControlParams(VehicleSyncNotification notification) + { + var parameters = new RotateControlParams + { + ActiveOmega = Conf.MultiVehicleRotateActiveOmega, + CompXyFac = Conf.MultiVehicleRotateCompXyFac, + CompXyIFac = Conf.MultiVehicleRotateCompXyIFac, + CompXyMax = Conf.MultiVehicleRotateCompXyMax, + CompThFac = Conf.MultiVehicleRotateCompThFac, + CompThIFac = Conf.MultiVehicleRotateCompThIFac, + CompThMax = Conf.MultiVehicleRotateCompThMax, + CompTangentFrac = NormalizeRotateCompTangentFrac(Conf.MultiVehicleRotateCompTangentFrac), + StartWheelAlignDeg = Conf.InPlaceRotateWheelAlignDeg, + ActiveWheelAlignDeg = NormalizeRotateWheelAlignDeg(Conf.InPlaceRotateActiveWheelAlignDeg, + Conf.InPlaceRotateWheelAlignDeg) + }; + + if (notification == null || !notification.RotateParamsValid) + return parameters; + + parameters.ActiveOmega = notification.RotateActiveOmega; + parameters.CompXyFac = notification.RotateCompXyFac; + parameters.CompXyIFac = notification.RotateCompXyIFac; + parameters.CompXyMax = notification.RotateCompXyMax; + parameters.CompThFac = notification.RotateCompThFac; + parameters.CompThIFac = notification.RotateCompThIFac; + parameters.CompThMax = notification.RotateCompThMax; + parameters.CompTangentFrac = NormalizeRotateCompTangentFrac(notification.RotateCompTangentFrac); + parameters.StartWheelAlignDeg = NormalizeRotateWheelAlignDeg(notification.RotateStartWheelAlignDeg, + Conf.InPlaceRotateWheelAlignDeg); + parameters.ActiveWheelAlignDeg = NormalizeRotateWheelAlignDeg(notification.RotateActiveWheelAlignDeg, + parameters.StartWheelAlignDeg); + return parameters; + } + + private static void FillRotateNotificationParams(VehicleSyncNotification notification, RotateControlParams parameters) + { + notification.RotateParamsValid = true; + notification.RotateActiveOmega = parameters.ActiveOmega; + notification.RotateCompXyFac = parameters.CompXyFac; + notification.RotateCompXyIFac = parameters.CompXyIFac; + notification.RotateCompXyMax = parameters.CompXyMax; + notification.RotateCompThFac = parameters.CompThFac; + notification.RotateCompThIFac = parameters.CompThIFac; + notification.RotateCompThMax = parameters.CompThMax; + notification.RotateCompTangentFrac = parameters.CompTangentFrac; + notification.RotateStartWheelAlignDeg = parameters.StartWheelAlignDeg; + notification.RotateActiveWheelAlignDeg = parameters.ActiveWheelAlignDeg; + } + + private static float NormalizeRotateWheelAlignDeg(float value, float fallback) + { + if (float.IsNaN(value) || float.IsInfinity(value) || value <= 0) + return fallback; + return value; + } + + private static float NormalizeRotateCompTangentFrac(float frac) + { + if (float.IsNaN(frac) || float.IsInfinity(frac) || frac < 0) + return DefaultRotateCompTangentFrac; + return frac; + } + + private void LimitRotateCompensation(ref float compVx, ref float compVy, ref float compOmega, + float absOmega, float controlRadius, float tangentFrac) + { + var frac = NormalizeRotateCompTangentFrac(tangentFrac); + if (frac < 0 || absOmega <= 1e-6f) return; + + var rawVx = compVx; + var rawVy = compVy; + var rawOmega = compOmega; + var tangentMmps = absOmega / 180f * (float)Math.PI * Math.Max(1f, Math.Abs(controlRadius)); + var xyLimit = tangentMmps * frac; + var xyMag = (float)Math.Sqrt(compVx * compVx + compVy * compVy); + if (xyMag > xyLimit && xyMag > 1e-6f) + { + var scale = xyLimit / xyMag; + compVx *= scale; + compVy *= scale; + } + + var omegaLimit = absOmega * frac; + compOmega = ClampBias(compOmega, omegaLimit); + + var changed = Math.Abs(rawVx - compVx) > 1e-3f || + Math.Abs(rawVy - compVy) > 1e-3f || + Math.Abs(rawOmega - compOmega) > 1e-3f; + var now = DateTime.Now; + if (changed && (now - _mvRotateCompLimitLastLog).TotalMilliseconds >= 200) + { + _mvRotateCompLimitLastLog = now; + DLog.Log( + $"car{CarNum} ROTATE_COMP_LIMIT frac={frac:0.00} omega={absOmega:0.000} radius={controlRadius:0} " + + $"tan={tangentMmps:0.0} xyLimit={xyLimit:0.0} omLimit={omegaLimit:0.000} " + + $"raw=({rawVx:0.0},{rawVy:0.0},{rawOmega:0.000}) " + + $"limited=({compVx:0.0},{compVy:0.0},{compOmega:0.000})", + "MultiVehicleRemoteDbg"); + } + } + + private void ResetMultiVehicleRotateCenterDrift() + { + _mvRotateCenterDriftActive = false; + _mvRotateCenterMaxDrift = 0; + _mvRotateCenterLastLog = DateTime.MinValue; + } + + private void LogMultiVehicleRotateCenterDrift(bool isMaster, bool manualEnabled, bool autoEnabled, + bool fleetReady, bool canMove, bool rotating, float centerX, float centerY, float centerTh, + float requestedOmega, float fleetOmega, float compVx, float compVy, float compOmega) + { + if (!_mvRotateCenterDriftActive) + { + _mvRotateCenterDriftActive = true; + _mvRotateCenterStartX = centerX; + _mvRotateCenterStartY = centerY; + _mvRotateCenterStartTh = centerTh; + _mvRotateCenterMaxDrift = 0; + _mvRotateCenterLastLog = DateTime.MinValue; + } + + var dx = centerX - _mvRotateCenterStartX; + var dy = centerY - _mvRotateCenterStartY; + var drift = (float)Math.Sqrt(dx * dx + dy * dy); + _mvRotateCenterMaxDrift = Math.Max(_mvRotateCenterMaxDrift, drift); + var dth = (float)CommonMath.ThDiff(centerTh, _mvRotateCenterStartTh); + var now = DateTime.Now; + if ((now - _mvRotateCenterLastLog).TotalMilliseconds < 250) + return; + + _mvRotateCenterLastLog = now; + DLog.Log( + $"car{CarNum} CTRL master={isMaster} manual={manualEnabled} auto={autoEnabled} " + + $"ready={fleetReady} canMove={canMove} rotating={rotating} " + + $"center=({centerX:0.0},{centerY:0.0},{centerTh:0.00}) " + + $"start=({_mvRotateCenterStartX:0.0},{_mvRotateCenterStartY:0.0},{_mvRotateCenterStartTh:0.00}) " + + $"drift=({dx:0.0},{dy:0.0}) dist={drift:0.0} max={_mvRotateCenterMaxDrift:0.0} dth={dth:0.00} " + + $"omegaReq={requestedOmega:0.000} omega={fleetOmega:0.000} comp=({compVx:0.0},{compVy:0.0},{compOmega:0.000})", + "FleetRotateCenterDbg"); + } + + /// + /// 原地旋转纠偏单轴 PI 控制器。err 为车体系偏差(mm 或 deg),输出为修正速度(mm/s 或 deg/s), + /// 带死区、积分抗饱和(限制积分贡献在 ±max 内)与总输出限幅。死区内冻结积分(保留稳态修正以抵消恒定扰动)。 + /// + private static float RotatePiTerm(float err, ref float integ, float deadband, + float pFac, float iFac, float max, float dt, bool allowIntegrate = true) + { + // 死区内:不再累加误差,仅输出已积累的积分项(维持对恒定扰动的稳态补偿)。 + if (Math.Abs(err) < deadband) + return ClampBias(integ * iFac, max); + + // 条件积分(抗 windup):仅当 (a) 允许积分(舵轮已对齐、车在真正转动) 且 + // (b) 总输出未在同向饱和 时才累加误差。 + // allowIntegrate=false:上一拍舵轮未对齐(gate=0、车没动),此时积分误差是纯 windup, + // 会让纠偏越积越大、舵轮更对不齐 → 原地卡死;故冻结积分(保留已有值,仅输出 P+已积分)。 + // 同向饱和停积分:起步大误差让 P 顶满 max 时继续积分会顶满积分器,误差反向后泄放慢、造成过冲回拉。 + var unclamped = err * pFac + integ * iFac; + var saturatedSameSign = Math.Abs(unclamped) >= max && Math.Sign(unclamped) == Math.Sign(err); + if (allowIntegrate && !saturatedSameSign) + integ += err * dt; + + var iTerm = integ * iFac; + // 抗积分饱和:把积分贡献限制在 ±max,并反算回写积分器,避免 windup。 + if (iFac > 1e-9f) + { + var iLimit = max / iFac; + if (integ > iLimit) integ = iLimit; + else if (integ < -iLimit) integ = -iLimit; + iTerm = integ * iFac; + } + + return ClampBias(err * pFac + iTerm, max); + } + + /// + /// 主车专用:通过 Playground WebAPI 读取本车与邻车的真实 sim 位姿,落 RotatePoseDbg 日志。 + /// 量化"开环横向滑移"来源: + /// - w1/w2/dw:两车实际偏航角速率(deg/s)。dw 持续非零 ⇒ 主从转速不一致(相对旋转)。 + /// - rel_in1(x,y,dyaw):邻车在本车体系下的真实相对位姿;dyaw=实际相对朝向−180°。 + /// dyaw≈0 但 y 持续漂 ⇒ 纯横向平移滑移(非角度滞后,印证 B 无用)。 + /// - midDrift:车队几何中心(世界系)相对起转瞬间的位移 ⇒ 整体平移漂移量。 + /// 节流 ~200ms;WebAPI 异常时静默跳过,不影响控制环。 + /// + private void LogRotatePoseSample() + { + if (!Conf.MultiVehicleRotatePoseWebApiDiagEnabled) return; + + var now = DateTime.Now; + if ((now - _rotPoseLastLog).TotalMilliseconds < 200) return; + _rotPoseLastLog = now; + + var neighbor = Conf.PlaygroundNeighborRobotName; + if (string.IsNullOrWhiteSpace(neighbor) || neighbor == Conf.PlaygroundRobotName) return; + + try + { + var p1 = PlaygroundWebApi.GetPose(Conf.PlaygroundWebApiUrl, Conf.PlaygroundRobotName); + var p2 = PlaygroundWebApi.GetPose(Conf.PlaygroundWebApiUrl, neighbor); + + // 邻车在本车(car1)体系下的相对位姿:rel = R(-yaw1)·(p2-p1) + var ddx = p2.X - p1.X; + var ddy = p2.Y - p1.Y; + var th1 = p1.YawDeg * (float)Math.PI / 180f; + var c = (float)Math.Cos(th1); + var s = (float)Math.Sin(th1); + var relX = ddx * c + ddy * s; + var relY = -ddx * s + ddy * c; + var relYaw = (float)LessMath.ThDiff(p2.YawDeg - p1.YawDeg, 180f); // 实际相对朝向偏离 180° 的量 + var dist = (float)Math.Sqrt(ddx * ddx + ddy * ddy); + + var midX = (p1.X + p2.X) / 2f; + var midY = (p1.Y + p2.Y) / 2f; + if (!_rotPoseEpisode) + { + _rotPoseEpisode = true; + _rotPoseMid0X = midX; + _rotPoseMid0Y = midY; + } + var midDx = midX - _rotPoseMid0X; + var midDy = midY - _rotPoseMid0Y; + + // 实际偏航角速率(数值微分) + float w1 = 0, w2 = 0, dw = 0; + if (_rotPosePrevTime != DateTime.MinValue) + { + var dt = (float)(now - _rotPosePrevTime).TotalSeconds; + if (dt > 1e-3) + { + w1 = (float)LessMath.ThDiff(p1.YawDeg, _rotPosePrevYaw1) / dt; + w2 = (float)LessMath.ThDiff(p2.YawDeg, _rotPosePrevYaw2) / dt; + dw = w1 - w2; + } + } + _rotPosePrevTime = now; + _rotPosePrevYaw1 = p1.YawDeg; + _rotPosePrevYaw2 = p2.YawDeg; + + DLog.Log( + $"car1({p1.X:F0},{p1.Y:F0},{p1.YawDeg:F2}) car2({p2.X:F0},{p2.Y:F0},{p2.YawDeg:F2}) " + + $"| rel_in1(x:{relX:F0} y:{relY:F0} dyaw:{relYaw:F2}) dist:{dist:F0} " + + $"| mid({midX:F0},{midY:F0}) midDrift(dx:{midDx:F0} dy:{midDy:F0} |d|:{Math.Sqrt(midDx * midDx + midDy * midDy):F0}) " + + $"| w1:{w1:F2} w2:{w2:F2} dw:{dw:F2}", + "RotatePoseDbg"); + } + catch (Exception e) + { + // 诊断用途,WebAPI 不通时静默(仅偶发提示),不打断控制环。 + Hedingben.ToastText($"RotatePose WebAPI err: {e.Message}", $"MultiVehicle{CarNum}-rotpose"); + } + } + + #endregion +} diff --git a/.task8-sweep/PlaygroundWebApi.cs b/.task8-sweep/PlaygroundWebApi.cs new file mode 100644 index 0000000..03ace8e --- /dev/null +++ b/.task8-sweep/PlaygroundWebApi.cs @@ -0,0 +1,81 @@ +using System; +using System.Net.Http; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MultiWheelC; + +/// +/// Playground 仿真器 HTTP Web API 轻量客户端:查询小车位姿、瞬移小车。 +/// 服务端实现见 Playground/Web/PlaygroundWebApi.cs,默认监听 http://localhost:18090。 +/// 坐标单位 mm,朝向 yawDeg 单位为度,世界坐标系与场景 JSON 一致。 +/// +public static class PlaygroundWebApi +{ + // 禁用系统代理:本机 Playground 走 localhost,若经系统代理(如 127.0.0.1:7890)会连接失败。 + private static readonly HttpClient Http = new HttpClient(new HttpClientHandler { UseProxy = false }) + { + Timeout = TimeSpan.FromSeconds(3) + }; + + public struct Pose + { + public float X; + public float Y; + public float YawDeg; + } + + /// 查询单台小车的世界位姿。GET /api/robots/{name}。 + public static Pose GetPose(string baseUrl, string robotName) + { + var url = $"{baseUrl.TrimEnd('/')}/api/robots/{Uri.EscapeDataString(robotName)}"; + var json = Http.GetStringAsync(url).GetAwaiter().GetResult(); + var o = JObject.Parse(json); + return new Pose + { + X = o.Value("x"), + Y = o.Value("y"), + YawDeg = o.Value("yawDeg") + }; + } + + /// 将小车瞬移到目标世界位姿。POST /api/robots/{name}/move。 + public static void Move(string baseUrl, string robotName, float x, float y, float yawDeg) + { + var url = $"{baseUrl.TrimEnd('/')}/api/robots/{Uri.EscapeDataString(robotName)}/move"; + var body = JsonConvert.SerializeObject(new { x, y, yaw = yawDeg, stop = true }); + using var content = new StringContent(body, Encoding.UTF8, "application/json"); + var resp = Http.PostAsync(url, content).GetAwaiter().GetResult(); + resp.EnsureSuccessStatusCode(); + } + + /// 查询车辆运动是否启用(暂停时为 false)。GET /api/motion。 + public static bool MotionEnabled(string baseUrl) + { + var url = $"{baseUrl.TrimEnd('/')}/api/motion"; + var json = Http.GetStringAsync(url).GetAwaiter().GetResult(); + return JObject.Parse(json).Value("motionEnabled"); + } + + /// 恢复车辆运动。POST /api/motion/resume。 + public static void ResumeMotion(string baseUrl) + { + var url = $"{baseUrl.TrimEnd('/')}/api/motion/resume"; + var resp = Http.PostAsync(url, null).GetAwaiter().GetResult(); + resp.EnsureSuccessStatusCode(); + } + + /// + /// 暂停车辆运动(仅冻结运动,不停止仿真;传感器继续扫描)。POST /api/motion/pause。 + /// feedback: "zero"(默认,反馈归零) / "none"(不上报) / "hold"(保留暂停瞬间值)。 + /// + public static void PauseMotion(string baseUrl, string feedback = "zero") + { + var url = $"{baseUrl.TrimEnd('/')}/api/motion/pause"; + var body = JsonConvert.SerializeObject(new { feedback }); + using var content = new StringContent(body, Encoding.UTF8, "application/json"); + var resp = Http.PostAsync(url, content).GetAwaiter().GetResult(); + resp.EnsureSuccessStatusCode(); + } +} diff --git a/.task8-sweep/TireFollowing.cs b/.task8-sweep/TireFollowing.cs new file mode 100644 index 0000000..049d572 --- /dev/null +++ b/.task8-sweep/TireFollowing.cs @@ -0,0 +1,511 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading; +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using ClumsyCore.Utilities; +using ClumsyDance.ClumsyWalk.Detectors; +using CommonUsage.Chassis; +using FundamentalLib; +using MDCSToolBox; +using MDCSToolBox.Clumsy.Calibration; +using MDCSToolBox.Clumsy.MotionControllers; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MDCSToolBox.Clumsy.Tracks; +using MDCSToolBox.Commons.Controllers; +using static ClumsyCore.DTools.Painter; +using LineSegment = ClumsyCore.Utilities.LineSegment; + +namespace MultiWheelC +{ + public class TireFollowing : MovementDefinition + { + public Func GetController; + + /// + /// 车辆方向 + /// + public float CarDirection = 0; + + /// + /// 停止距离 + /// + //public float FinishDistance = 1000; + + /// + /// 减速距离 + /// + public float SlowDistance = 1000; + + /// + /// 最大速度 + /// + public float MaxSpeed = 0.3f; + + // 末段衔接:接近盲走终点时给非零速度,供后续动作连续接管 + public bool EnableHandover = false; + public float HandoverDistance = 200f; // mm + public float HandoverSpeed = 0.2f; // m/s + + /// + /// 钻轮胎数量 + /// + public int TireNum = 1; + + /// + /// 盲走角度偏移 + /// + public float WalkBlindTh = -1f; + + /// + /// 是否检测到目标 + /// + public bool NoTarget = false; + + public float GuessRangeX; + + public float GuessRangeY; + + /// + /// 检测器定义 + /// + public class DetectorDefinition + { + /// + /// 开始检测距离 + /// + public float StartGuessingX; + + /// + /// 开始检测距离 + /// + public float StartGuessingY; + + /// + /// 检测函数 + /// + public Func, LineSegment> DetectFunction = null; + + public Action LeaveSrcFunction = null; + + public int SrcId = -1; + public int DstId = -1; + + /// + /// 路径偏移 + /// + public Tuple PathTransformation = Tuple.Create(0f, 0f, 0f); + + public float PathTransformationAnchorDistance = 0f; + + /// + /// 切换条件 + /// + public Func SwitchWalkBlindCondition = null; + + /// + /// 盲走停止距离 + /// + public Func FinishWalkBlindCondition = null; + } + + public Func FinishCondition; + + /// + /// 多个检测器列表 + /// + public List detectors = null; + + private Painter _painter; + private List _remainDistanceList = new List(); + private List _remainAngleList = new List(); + private List _targetYList = new List(); + + private List SetFilters(float guessCenterX, float guessCenterY) + { + var painter = UI.GetPainter("GeneralFollowing.SetFilters", false); + painter.Clear(); + painter.Clear(3000); + + var box = new Vector2[] + { + new (guessCenterX - GuessRangeX, guessCenterY - GuessRangeY), + new (guessCenterX + GuessRangeX, guessCenterY - GuessRangeY), + new (guessCenterX + GuessRangeX, guessCenterY + GuessRangeY), + new (guessCenterX - GuessRangeX, guessCenterY + GuessRangeY), + }; + + for (var i = 0; i < box.Length; ++i) + painter.DrawLine(Color.DarkOliveGreen, box[i], box[(i + 1) % 4]); + + // PC filter in car coordinate frame + return new List() + { + new(CoordinateSystem.Car2D, + p => LessMath.IsPointInPolygon4( + box.Select(v => new PointF(v.X, v.Y)).ToArray(), new PointF(p.X, p.Y))), + }; + } + + public void Stop() + { + _dt?.Stop(); + } + + /// + ///计算车体中心的位移和角度增量 + /// + /// a轮在车体坐标系下位置 + /// a轮在车体坐标系下位移增量 + /// b轮在车体坐标系下位置 + /// b轮在车体坐标系下位移增量 + /// + private static (float, float, float) CenterMoveFromPoints(Vector2 a, + Vector2 aDelta, + Vector2 b, + Vector2 bDelta) + { + float th_x = 0, th_y = 0, th = 0, x = 0, y = 0; + var eps = 0.0000001; + if (Math.Abs(a.Y - b.Y) > eps) + { + th_x = (aDelta.X - bDelta.X) / (b.Y - a.Y); + } + if (Math.Abs(a.X - b.X) > eps) + { + th_y = (aDelta.Y - bDelta.Y) / (a.X - b.X); + } + th = th_x == 0 ? th_y : th_x; + + x = (aDelta.X + bDelta.X) / 2f - (a.Y - b.Y) / 2f * th; + + y = (aDelta.Y + bDelta.Y) / 2f + (a.X - b.X) / 2f * th; + + return (x, y, th); + } + + + public override IEnumerable Get() + { + _painter = UI.GetPainter("GeneralFollowing", false); + var lastDetectX = detectors[0].StartGuessingX; + var lastDetectY = detectors[0].StartGuessingY; + var detectorIndex = 0; + + var controller = (MultiWheelGeometricController)GetController.Invoke(); + controller.BaseSpeed = MaxSpeed; + controller.FinishDistance = float.MinValue; + controller.FirstThAccuracy = 999; + _dt = new DriveTask(controller.Track(true, CoordinateSystem.Car2D)); + void HardStop() + { + _dt?.Stop(); + ((MultiWheelChassis)PilotDefinition.Chassis).DriveStop(); + DLog.Log($"Hard Stop!", "TireFollowing"); + } + float WalkBlindCarPathDstX = -1f, WalkBlindCarPathDstY = -1f, WalkBlindCarPathDstTh = -1f; + bool WalkBlindStage1 = false, WalkBlindStage2 = false; + var angle2target = -1f; + float _lastLFLEncoder = -1, _lastLFREncoder = -1, _lastRFLEncoder = -1, _lastRFREncoder = -1; + float _lastLRLEncoder = -1, _lastLRREncoder = -1, _lastRRLEncoder = -1, _lastRRREncoder = -1; + + (float, float, float) GetCurrentPos2Dst(float lastX, float lastY, float lastTh) + { + // Read current encoders + var curLFLEncoder = PilotDefinition.Self.LFLActualPos; + var curLFREncoder = PilotDefinition.Self.LFRActualPos; + var curRFLEncoder = PilotDefinition.Self.RFLActualPos; + var curRFREncoder = PilotDefinition.Self.RFRActualPos; + var curLRLEncoder = PilotDefinition.Self.LRLActualPos; + var curLRREncoder = PilotDefinition.Self.LRRActualPos; + var curRRLEncoder = PilotDefinition.Self.RRLActualPos; + var curRRREncoder = PilotDefinition.Self.RRRActualPos; + + // Average delta per wheel pair (LF, LR, RF, RR) + var lfDelta = (curLFLEncoder - _lastLFLEncoder + curLFREncoder - _lastLFREncoder) / 2f; + var lrDelta = (curLRLEncoder - _lastLRLEncoder + curLRREncoder - _lastLRREncoder) / 2f; + var rfDelta = (curRFLEncoder - _lastRFLEncoder + curRFREncoder - _lastRFREncoder) / 2f; + var rrDelta = (curRRLEncoder - _lastRRLEncoder + curRRREncoder - _lastRRREncoder) / 2f; + var deltaList = new List { lfDelta, lrDelta, rfDelta, rrDelta }; + + var xs = new List(); + var ys = new List(); + var ths = new List(); + var chassis = (MultiWheelChassis)BasicPilotBase.Chassis; + var steerWheels = chassis.GetSteerWheels(); + for (var i = 0; i < steerWheels.Count; ++i) + { + var sw1 = steerWheels[i]; + var a = sw1.Position; + var tha = sw1.ReadAngle() / 180f * (float)Math.PI; + var deltaa = deltaList[i]; + var va = new Vector2(deltaa * (float)Math.Cos(tha), deltaa * (float)Math.Sin(tha)); + for (var j = i + 1; j < steerWheels.Count; ++j) + { + var sw2 = steerWheels[j]; + var b = sw2.Position; + var thb = sw2.ReadAngle() / 180f * (float)Math.PI; + var deltab = deltaList[j]; + var vb = new Vector2(deltab * (float)Math.Cos(thb), deltab * (float)Math.Sin(thb)); + var (tempx, tempy, tempth) = CenterMoveFromPoints(a, va, b, vb); + Hedingben.ToastText($"{tempx:f2} {tempy:f2} {tempth / Math.PI * 180f:f2} ", $"{i}_{j}"); + xs.Add(tempx); + ys.Add(tempy); + ths.Add(tempth); + } + } + var x = xs.Average(); + var y = ys.Average(); + var Th = ths.Average() / (float)Math.PI * 180; + var moveTup = Tuple.Create(x, y, Th); + var moved = MathTools.SolveTransform2D(MathTools.SolveTransform2D(Tuple.Create(lastX, lastY, lastTh), moveTup), Tuple.Create(0f, 0f, 0f)); + + _lastLFLEncoder = curLFLEncoder; + _lastLFREncoder = curLFREncoder; + _lastRFLEncoder = curRFLEncoder; + _lastRFREncoder = curRFREncoder; + _lastLRLEncoder = curLRLEncoder; + _lastLRREncoder = curLRREncoder; + _lastRRLEncoder = curRRLEncoder; + _lastRRREncoder = curRRREncoder; + return (moved.Item1, moved.Item2, moved.Item3); + } + + while (true) + { + if (detectorIndex > detectors.Count - 1) + throw new Exception("detector index out of range!"); + + _painter.Clear(); + if (WalkBlindStage1 || WalkBlindStage2) + { + //第二次盲走时或只钻一个轮胎时 + if (WalkBlindStage2 || detectors.Count == 1 || TireNum == 1) + { + //controller.FinishDistance = 10f; + controller.SlowDistance = SlowDistance; + controller.SlowingPow = 0.7f; + } + if (EnableHandover) + { + controller.SlowDistance = float.MinValue; + controller.FinishSpeed = 0.2f; + controller.FinishDistance = 50; + } + (WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh) = GetCurrentPos2Dst(WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh); + var walkBlindPathEnd = Tuple.Create(WalkBlindCarPathDstX, WalkBlindCarPathDstY, WalkBlindCarPathDstTh); + var walkBlindPathStart = LessMath.Transform2D(walkBlindPathEnd, Tuple.Create(CarDirection == 0 ? -3000f : 3000f, 0f, 0f)); + var walkBlindPathDst = new Vector2(WalkBlindCarPathDstX, WalkBlindCarPathDstY); + var walkBlindPathSrc = new Vector2(walkBlindPathStart.Item1, walkBlindPathStart.Item2); + var walkBlindPath = new LineSegment(walkBlindPathSrc, walkBlindPathDst); + + DLog.Log($"盲走目标点:{walkBlindPath.Src.X:F2} {walkBlindPath.Src.Y:F2} {walkBlindPath.Dst.X:F2} {walkBlindPath.Dst.Y:F2}", "TireFollowing"); + _painter.DrawDot(Color.Purple, walkBlindPathDst, sz: 3); + _painter.DrawLine(Color.GreenYellow, walkBlindPath.Src, walkBlindPath.Dst, endArrow: true, width: 2); + + var track = new LineTrack(walkBlindPath.Src, walkBlindPath.Dst); + track.CarDirectionBias = CarDirection; + controller.UpdateTracks(new List { track }); + var rd = (float)LessMath.PerpendicularPosition(0, 0, walkBlindPath.Dst.X, walkBlindPath.Dst.Y, + walkBlindPath.Src.X, walkBlindPath.Src.Y); + _remainDistanceList.Add(rd); + while (_remainDistanceList.Count > 3) _remainDistanceList.RemoveAt(0); + rd = _remainDistanceList.Average(); + DLog.Log($"盲走投影点剩余距离:{rd:0.0} ", "TireFollowing"); + // 检查是否达到盲走结束条件 + if (detectors[detectorIndex].FinishWalkBlindCondition(rd)) + { + if (WalkBlindStage1) + { + DLog.Log("达到第一次盲走停止距离,停下或开始钻第二对轮胎", "TireFollowing"); + //if (detectors[detectorIndex].DstId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) + //{ + // detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].DstId); + // DLog.Log($"释放取车点{detectors[detectorIndex].DstId}", "TireFollowing"); + //} + WalkBlindStage1 = false; + _remainAngleList.Clear(); + _remainDistanceList.Clear(); + detectorIndex++; + if ((detectors.Count == 1 || TireNum == 1) && !EnableHandover) + { + HardStop(); + yield return false; + } + } + else if (WalkBlindStage2) + { + DLog.Log("达到第二对轮胎处,停止移动", "TireFollowing"); + if (!EnableHandover) + { + HardStop(); + } + yield return false; + } + } + yield return true; + continue; + } + + var target = detectors[detectorIndex].DetectFunction(CarDirection, lastDetectX, + SetFilters(lastDetectX, lastDetectY)); + + if (target == null) + { + DLog.Log("无目标,等待下一帧", "TireFollowing"); + controller.FirstRotateMaxSpeed = 0; + yield return true; + continue; + } + else controller.FirstRotateMaxSpeed = 5; + + var targetAngle = CalculateAngle2YAxis(target.Src, target.Dst); + var targetPos = new Vector2((target.Src.X + target.Dst.X) / 2f, (target.Src.Y + target.Dst.Y) / 2f); + var dis2target = (float)Math.Sqrt(Math.Pow(targetPos.X, 2) + Math.Pow(targetPos.Y, 2)); + //距离较近以后角度容易跳变 + if (dis2target < PilotDefinition.Conf.TireFollowingCloseDistance && Math.Abs(targetAngle) > PilotDefinition.Conf.TireFollowingAngleIgnoreThr) + { + yield return true; + continue; + } + else _remainAngleList.Add(targetAngle); + while (_remainAngleList.Count > 10) _remainAngleList.RemoveAt(0); + angle2target = _remainAngleList.Average(); + var distanceLabelPos = targetPos / 2f; + _painter.DrawLine(Color.Cyan, Vector2.Zero, targetPos, width: 2); + _painter.DrawText(Color.Yellow, $"{dis2target:F3}", distanceLabelPos.X, distanceLabelPos.Y); + + var path = DetectorHelper.GetApproachPath(target, CoordinateSystem.Car2D, pathLen: 3000, + bias: detectors[detectorIndex].PathTransformation, + biasAnchorDistance: detectors[detectorIndex].PathTransformationAnchorDistance); + + if (path == null) + { + DLog.Log("no path!", "TireFollowing"); + NoTarget = true; + } + else + { + lastDetectX = ((target.Src + target.Dst) / 2f).X; + lastDetectY = ((target.Src + target.Dst) / 2f).Y; + + var currentY = path.CarPath.Dst.Y; + if (Math.Abs(targetAngle) < PilotDefinition.Conf.TireFollowingAngleIgnoreThr && + dis2target < PilotDefinition.Conf.TireFollowingCloseDistance) + { + _targetYList.Add(currentY); + while (_targetYList.Count > PilotDefinition.Conf.TireFollowingYAverageFrameCount) _targetYList.RemoveAt(0); + } + + var trackDstY = _targetYList.Count > 0 ? _targetYList.Average() : currentY; + Hedingben.ToastText($"target Y:{_targetYList.Count} {trackDstY}", "target Y"); + + var trackDst = new Vector2(path.CarPath.Dst.X, trackDstY); + _painter.DrawLine(Color.GreenYellow, path.CarPath.Src, trackDst, endArrow: true); + + var rd = (float)LessMath.PerpendicularPosition(0, 0, trackDst.X, trackDst.Y, + path.CarPath.Src.X, path.CarPath.Src.Y); + _remainDistanceList.Add(rd); + while (_remainDistanceList.Count > 3) _remainDistanceList.RemoveAt(0); + rd = _remainDistanceList.Average(); + _painter.DrawText(Color.Green, $"{rd:F3}", distanceLabelPos.X, distanceLabelPos.Y - 200); + if(rd < PilotDefinition.Conf.TireFollowingReleaseDistance) + { + if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) + { + detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); + DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); + } + } + + if (detectorIndex < detectors.Count - 1) + { + controller.SlowDistance = 1; + if (detectors[detectorIndex].SwitchWalkBlindCondition(rd)) + { + WalkBlindStage1 = true; + //if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) + //{ + // detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); + // DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); + //} + WalkBlindCarPathDstX = trackDst.X; + WalkBlindCarPathDstY = trackDst.Y; + WalkBlindCarPathDstTh = angle2target + WalkBlindTh; + DLog.Log($"切换至第一次盲走时刻目标点:{WalkBlindCarPathDstX:F2} " + + $"{WalkBlindCarPathDstY:F2} " + + $"{WalkBlindCarPathDstTh:F2}", "TireFollowing"); + _lastLFLEncoder = PilotDefinition.Self.LFLActualPos; + _lastLFREncoder = PilotDefinition.Self.LFRActualPos; + _lastRFLEncoder = PilotDefinition.Self.RFLActualPos; + _lastRFREncoder = PilotDefinition.Self.RFRActualPos; + _lastLRLEncoder = PilotDefinition.Self.LRLActualPos; + _lastLRREncoder = PilotDefinition.Self.LRRActualPos; + _lastRRLEncoder = PilotDefinition.Self.RRLActualPos; + _lastRRREncoder = PilotDefinition.Self.RRRActualPos; + _remainDistanceList.Clear(); + _targetYList.Clear(); + lastDetectX = detectors[detectorIndex + 1].StartGuessingX; + lastDetectY = detectors[detectorIndex + 1].StartGuessingY; + continue; + } + } + else if (detectorIndex == detectors.Count - 1) + { + if (detectors[detectorIndex].SwitchWalkBlindCondition(rd)) + { + WalkBlindStage2 = true; + WalkBlindCarPathDstX = trackDst.X; + WalkBlindCarPathDstY = trackDst.Y; + WalkBlindCarPathDstTh = angle2target + WalkBlindTh; + DLog.Log($"切换至最后一次盲走时刻目标点:{WalkBlindCarPathDstX:F2} " + + $"{WalkBlindCarPathDstY:F2} " + + $"{WalkBlindCarPathDstTh:F2}", "TireFollowing"); + if (detectors.Count == 1) + { + if (detectors[detectorIndex].SrcId != -1 && detectors[detectorIndex].LeaveSrcFunction != null) + { + detectors[detectorIndex].LeaveSrcFunction(detectors[detectorIndex].SrcId); + DLog.Log($"释放预取车点{detectors[detectorIndex].SrcId}", "TireFollowing"); + } + } + + _lastLFLEncoder = PilotDefinition.Self.LFLActualPos; + _lastLFREncoder = PilotDefinition.Self.LFRActualPos; + _lastRFLEncoder = PilotDefinition.Self.RFLActualPos; + _lastRFREncoder = PilotDefinition.Self.RFRActualPos; + _lastLRLEncoder = PilotDefinition.Self.LRLActualPos; + _lastLRREncoder = PilotDefinition.Self.LRRActualPos; + _lastRRLEncoder = PilotDefinition.Self.RRLActualPos; + _lastRRREncoder = PilotDefinition.Self.RRRActualPos; + _remainDistanceList.Clear(); + _targetYList.Clear(); + continue; + } + } + DLog.Log($"投影点剩余距离:{rd:F2}", "TireFollowing"); + var track = new LineTrack(path.CarPath.Src, trackDst); + track.CarDirectionBias = CarDirection; + controller.UpdateTracks(new List { track }); + NoTarget = false; + } + yield return true; + } + } + + private static float CalculateAngle2YAxis(Vector2 point1, Vector2 point2) + { + return -(float)(Math.Atan((point1.X - point2.X) / (point1.Y - point2.Y)) * 180 / Math.PI); + } + + private DriveTask _dt; + } +} diff --git a/.task8-sweep/VehicleSyncBinaryCodec.cs b/.task8-sweep/VehicleSyncBinaryCodec.cs new file mode 100644 index 0000000..e7bd154 --- /dev/null +++ b/.task8-sweep/VehicleSyncBinaryCodec.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace MultiWheelC; + +internal static class VehicleSyncBinaryCodec +{ + private const byte Version = 2; + private const byte RegisterType = 1; + private const byte NotificationType = 2; + private static readonly byte[] Magic = Encoding.ASCII.GetBytes("MVS1"); + + public static byte[] EncodeRegister(int carNum, VehicleSyncInfo info) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream, Encoding.UTF8); + WriteHeader(writer, RegisterType); + writer.Write(carNum); + WriteInfo(writer, info); + writer.Flush(); + return stream.ToArray(); + } + + public static (int CarNum, VehicleSyncInfo Info) DecodeRegister(byte[] payload) + { + using var stream = new MemoryStream(payload ?? throw new ArgumentNullException(nameof(payload))); + using var reader = new BinaryReader(stream, Encoding.UTF8); + var version = ReadHeader(reader, RegisterType); + var carNum = reader.ReadInt32(); + var info = ReadInfo(reader, version); + EnsureFullyRead(stream); + return (carNum, info); + } + + public static byte[] EncodeNotification(VehicleSyncNotification notification) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream, Encoding.UTF8); + WriteHeader(writer, NotificationType); + + writer.Write(notification.Seq); + writer.Write(BuildNotificationFlags(notification)); + writer.Write(notification.Mode); + writer.Write(notification.FleetStopSourceCar); + writer.Write(notification.CenterX); + writer.Write(notification.CenterY); + writer.Write(notification.CenterTh); + writer.Write(notification.FleetVx); + writer.Write(notification.FleetFrontTh); + writer.Write(notification.FleetRearTh); + writer.Write(notification.FleetOmega); + writer.Write(notification.RequestedFleetOmega); + writer.Write(notification.SyncTh); + writer.Write(notification.SyncDistance); + writer.Write(notification.DeltaDetectCenter); + writer.Write(notification.RotateActiveOmega); + writer.Write(notification.RotateCompXyFac); + writer.Write(notification.RotateCompXyIFac); + writer.Write(notification.RotateCompXyMax); + writer.Write(notification.RotateCompThFac); + writer.Write(notification.RotateCompThIFac); + writer.Write(notification.RotateCompThMax); + writer.Write(notification.RotateCompTangentFrac); + writer.Write(notification.RotateStartWheelAlignDeg); + writer.Write(notification.RotateActiveWheelAlignDeg); + writer.Write(notification.IdealX); + writer.Write(notification.IdealY); + writer.Write(notification.IdealTh); + WriteString(writer, notification.FleetStopReason); + + var fleet = notification.Fleet ?? new Dictionary(); + if (fleet.Count > ushort.MaxValue) + throw new InvalidOperationException($"Fleet count {fleet.Count} exceeds binary protocol limit."); + writer.Write((ushort)fleet.Count); + foreach (var kv in fleet) + { + writer.Write(kv.Key); + WriteInfo(writer, kv.Value); + } + + writer.Flush(); + return stream.ToArray(); + } + + public static VehicleSyncNotification DecodeNotification(byte[] payload) + { + using var stream = new MemoryStream(payload ?? throw new ArgumentNullException(nameof(payload))); + using var reader = new BinaryReader(stream, Encoding.UTF8); + var version = ReadHeader(reader, NotificationType); + + var notification = new VehicleSyncNotification + { + Seq = reader.ReadInt64() + }; + + ApplyNotificationFlags(notification, reader.ReadUInt16()); + notification.Mode = reader.ReadInt32(); + notification.FleetStopSourceCar = reader.ReadInt32(); + notification.CenterX = reader.ReadSingle(); + notification.CenterY = reader.ReadSingle(); + notification.CenterTh = reader.ReadSingle(); + notification.FleetVx = reader.ReadSingle(); + notification.FleetFrontTh = reader.ReadSingle(); + notification.FleetRearTh = reader.ReadSingle(); + notification.FleetOmega = reader.ReadSingle(); + notification.RequestedFleetOmega = reader.ReadSingle(); + notification.SyncTh = reader.ReadSingle(); + notification.SyncDistance = reader.ReadSingle(); + notification.DeltaDetectCenter = reader.ReadSingle(); + notification.RotateActiveOmega = reader.ReadSingle(); + notification.RotateCompXyFac = reader.ReadSingle(); + notification.RotateCompXyIFac = reader.ReadSingle(); + notification.RotateCompXyMax = reader.ReadSingle(); + notification.RotateCompThFac = reader.ReadSingle(); + notification.RotateCompThIFac = reader.ReadSingle(); + notification.RotateCompThMax = reader.ReadSingle(); + notification.RotateCompTangentFrac = reader.ReadSingle(); + notification.RotateStartWheelAlignDeg = reader.ReadSingle(); + notification.RotateActiveWheelAlignDeg = reader.ReadSingle(); + notification.IdealX = reader.ReadSingle(); + notification.IdealY = reader.ReadSingle(); + notification.IdealTh = reader.ReadSingle(); + notification.FleetStopReason = ReadString(reader); + + var fleetCount = reader.ReadUInt16(); + notification.Fleet = new Dictionary(fleetCount); + for (var i = 0; i < fleetCount; ++i) + { + var carNum = reader.ReadInt32(); + notification.Fleet[carNum] = ReadInfo(reader, version); + } + + EnsureFullyRead(stream); + return notification; + } + + private static void WriteHeader(BinaryWriter writer, byte type) + { + writer.Write(Magic); + writer.Write(Version); + writer.Write(type); + writer.Write((ushort)0); + } + + private static byte ReadHeader(BinaryReader reader, byte expectedType) + { + for (var i = 0; i < Magic.Length; ++i) + { + if (reader.ReadByte() != Magic[i]) + throw new InvalidDataException("Invalid multi-vehicle sync binary magic."); + } + + var version = reader.ReadByte(); + if (version < 1 || version > Version) + throw new InvalidDataException($"Unsupported multi-vehicle sync binary version {version}."); + + var type = reader.ReadByte(); + if (type != expectedType) + throw new InvalidDataException($"Unexpected multi-vehicle sync packet type {type}."); + + var reserved = reader.ReadUInt16(); + if (reserved != 0) + throw new InvalidDataException("Invalid multi-vehicle sync binary reserved field."); + + return version; + } + + private static void WriteInfo(BinaryWriter writer, VehicleSyncInfo info) + { + writer.Write(BuildInfoFlags(info)); + WriteString(writer, info.Ip); + writer.Write(info.Port); + writer.Write(info.X); + writer.Write(info.Y); + writer.Write(info.Th); + writer.Write(info.LayoutX); + writer.Write(info.LayoutY); + writer.Write(info.LayoutTh); + WriteString(writer, info.MotionInfeasibleReason); + WriteString(writer, info.RotateWheelAlignDetail); + writer.Write(info.AppliedNotificationSeq); + } + + private static VehicleSyncInfo ReadInfo(BinaryReader reader, byte version) + { + var info = new VehicleSyncInfo(); + ApplyInfoFlags(info, reader.ReadUInt16()); + info.Ip = ReadString(reader); + info.Port = reader.ReadInt32(); + info.X = reader.ReadSingle(); + info.Y = reader.ReadSingle(); + info.Th = reader.ReadSingle(); + info.LayoutX = reader.ReadSingle(); + info.LayoutY = reader.ReadSingle(); + info.LayoutTh = reader.ReadSingle(); + info.MotionInfeasibleReason = ReadString(reader); + info.RotateWheelAlignDetail = ReadString(reader); + info.AppliedNotificationSeq = version >= 2 ? reader.ReadInt64() : -1; + return info; + } + + private static ushort BuildInfoFlags(VehicleSyncInfo info) + { + ushort flags = 0; + if (info.Master) flags |= 1 << 0; + if (info.PosAvailable) flags |= 1 << 1; + if (info.Aligned) flags |= 1 << 2; + if (info.DetectOk) flags |= 1 << 3; + if (info.MotionFeasible) flags |= 1 << 4; + if (info.RotateWheelsAligned) flags |= 1 << 5; + return flags; + } + + private static void ApplyInfoFlags(VehicleSyncInfo info, ushort flags) + { + info.Master = (flags & (1 << 0)) != 0; + info.PosAvailable = (flags & (1 << 1)) != 0; + info.Aligned = (flags & (1 << 2)) != 0; + info.DetectOk = (flags & (1 << 3)) != 0; + info.MotionFeasible = (flags & (1 << 4)) != 0; + info.RotateWheelsAligned = (flags & (1 << 5)) != 0; + } + + private static ushort BuildNotificationFlags(VehicleSyncNotification notification) + { + ushort flags = 0; + if (notification.PosAvailable) flags |= 1 << 0; + if (notification.Aligned) flags |= 1 << 1; + if (notification.FleetMotionReleased) flags |= 1 << 2; + if (notification.FleetStopActive) flags |= 1 << 3; + if (notification.AutoEnabled) flags |= 1 << 4; + if (notification.ManualEnabled) flags |= 1 << 5; + if (notification.HasIdeal) flags |= 1 << 6; + if (notification.RotateParamsValid) flags |= 1 << 7; + if (notification.UseDetourCorrection) flags |= 1 << 8; + return flags; + } + + private static void ApplyNotificationFlags(VehicleSyncNotification notification, ushort flags) + { + notification.PosAvailable = (flags & (1 << 0)) != 0; + notification.Aligned = (flags & (1 << 1)) != 0; + notification.FleetMotionReleased = (flags & (1 << 2)) != 0; + notification.FleetStopActive = (flags & (1 << 3)) != 0; + notification.AutoEnabled = (flags & (1 << 4)) != 0; + notification.ManualEnabled = (flags & (1 << 5)) != 0; + notification.HasIdeal = (flags & (1 << 6)) != 0; + notification.RotateParamsValid = (flags & (1 << 7)) != 0; + notification.UseDetourCorrection = (flags & (1 << 8)) != 0; + } + + private static void WriteString(BinaryWriter writer, string value) + { + var bytes = Encoding.UTF8.GetBytes(value ?? ""); + if (bytes.Length > ushort.MaxValue) + throw new InvalidOperationException($"String payload length {bytes.Length} exceeds binary protocol limit."); + writer.Write((ushort)bytes.Length); + writer.Write(bytes); + } + + private static string ReadString(BinaryReader reader) + { + var length = reader.ReadUInt16(); + var bytes = reader.ReadBytes(length); + if (bytes.Length != length) + throw new EndOfStreamException("Truncated multi-vehicle sync string payload."); + return Encoding.UTF8.GetString(bytes); + } + + private static void EnsureFullyRead(MemoryStream stream) + { + if (stream.Position != stream.Length) + throw new InvalidDataException("Unexpected trailing bytes in multi-vehicle sync packet."); + } +} diff --git a/.task8-sweep/VehicleSyncModels.cs b/.task8-sweep/VehicleSyncModels.cs new file mode 100644 index 0000000..1c21428 --- /dev/null +++ b/.task8-sweep/VehicleSyncModels.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using ClumsyCore; +using Newtonsoft.Json; + +namespace MultiWheelC; + +public class VehicleSyncInfo +{ + [JsonProperty("Master")] public bool Master { get; set; } + [JsonProperty("Ip")] public string Ip { get; set; } = ""; + [JsonProperty("Port")] public int Port { get; set; } = 8008; + [JsonProperty("PosAvailable")] public bool PosAvailable { get; set; } + [JsonProperty("X")] public float X { get; set; } + [JsonProperty("Y")] public float Y { get; set; } + [JsonProperty("Th")] public float Th { get; set; } + [JsonProperty("LayoutX")] public float LayoutX { get; set; } + [JsonProperty("LayoutY")] public float LayoutY { get; set; } + [JsonProperty("LayoutTh")] public float LayoutTh { get; set; } + [JsonProperty("Aligned")] public bool Aligned { get; set; } + // 本车本轮是否成功识别到邻车(关闭互识别时恒为 true)。任一车为 false 则整队停车。 + [JsonProperty("DetectOk")] public bool DetectOk { get; set; } + [JsonProperty("MotionFeasible")] public bool MotionFeasible { get; set; } = true; + [JsonProperty("MotionInfeasibleReason")] public string MotionInfeasibleReason { get; set; } = ""; + [JsonProperty("RotateWheelsAligned")] public bool RotateWheelsAligned { get; set; } = true; + [JsonProperty("RotateWheelAlignDetail")] public string RotateWheelAlignDetail { get; set; } = ""; + [JsonProperty("AppliedNotificationSeq")] public long AppliedNotificationSeq { get; set; } = -1; +} + +public class VehicleSyncNotification +{ + [JsonProperty("PosAvailable")] public bool PosAvailable { get; set; } + [JsonProperty("CenterX")] public float CenterX { get; set; } + [JsonProperty("CenterY")] public float CenterY { get; set; } + [JsonProperty("CenterTh")] public float CenterTh { get; set; } + [JsonProperty("Aligned")] public bool Aligned { get; set; } + [JsonProperty("Fleet")] public Dictionary Fleet { get; set; } = new(); + [JsonProperty("FleetVx")] public float FleetVx { get; set; } + [JsonProperty("FleetFrontTh")] public float FleetFrontTh { get; set; } + [JsonProperty("FleetRearTh")] public float FleetRearTh { get; set; } + // 联动运动模式:0=常规(前进+转向) 1=蟹行(四轮同向平移) 2=原地旋转(绕车队中心) + [JsonProperty("Mode")] public int Mode { get; set; } + // 原地旋转角速度(deg/s,逆时针为正),仅 Mode==2 有效 + [JsonProperty("FleetOmega")] public float FleetOmega { get; set; } + [JsonProperty("RequestedFleetOmega")] public float RequestedFleetOmega { get; set; } + [JsonProperty("FleetMotionReleased")] public bool FleetMotionReleased { get; set; } = true; + [JsonProperty("FleetStopActive")] public bool FleetStopActive { get; set; } + [JsonProperty("FleetStopReason")] public string FleetStopReason { get; set; } = ""; + [JsonProperty("FleetStopSourceCar")] public int FleetStopSourceCar { get; set; } + [JsonProperty("AutoEnabled")] public bool AutoEnabled { get; set; } + [JsonProperty("ManualEnabled")] public bool ManualEnabled { get; set; } + [JsonProperty("UseDetourCorrection")] public bool UseDetourCorrection { get; set; } + [JsonProperty("SyncTh")] public float SyncTh { get; set; } + [JsonProperty("SyncDistance")] public float SyncDistance { get; set; } + [JsonProperty("DeltaDetectCenter")] public float DeltaDetectCenter { get; set; } + // 原地旋转纠偏参数由主车广播,从车运行时使用同一套增益/限幅,避免主从补偿强度不一致。 + [JsonProperty("RotateParamsValid")] public bool RotateParamsValid { get; set; } + [JsonProperty("RotateActiveOmega")] public float RotateActiveOmega { get; set; } + [JsonProperty("RotateCompXyFac")] public float RotateCompXyFac { get; set; } + [JsonProperty("RotateCompXyIFac")] public float RotateCompXyIFac { get; set; } + [JsonProperty("RotateCompXyMax")] public float RotateCompXyMax { get; set; } + [JsonProperty("RotateCompThFac")] public float RotateCompThFac { get; set; } + [JsonProperty("RotateCompThIFac")] public float RotateCompThIFac { get; set; } + [JsonProperty("RotateCompThMax")] public float RotateCompThMax { get; set; } + [JsonProperty("RotateCompTangentFrac")] public float RotateCompTangentFrac { get; set; } + [JsonProperty("RotateStartWheelAlignDeg")] public float RotateStartWheelAlignDeg { get; set; } + [JsonProperty("RotateActiveWheelAlignDeg")] public float RotateActiveWheelAlignDeg { get; set; } + // F: 单调递增序列号,从车据此丢弃乱序到达的旧 notify 包。 + [JsonProperty("Seq")] public long Seq { get; set; } + // D: 自动模式下主车路径控制器算出的车队中心理想位姿(世界系),由 idealPos/idealAngle 透传而来。 + // HasIdeal=true 时各从车按各自 layout 推算 per-car 目标位姿做前馈+补偿,弧线路径不再只靠事后纠偏。 + [JsonProperty("HasIdeal")] public bool HasIdeal { get; set; } + [JsonProperty("IdealX")] public float IdealX { get; set; } + [JsonProperty("IdealY")] public float IdealY { get; set; } + [JsonProperty("IdealTh")] public float IdealTh { get; set; } +} diff --git a/.task8-sweep/ref/CommonUsage.dll b/.task8-sweep/ref/CommonUsage.dll new file mode 100644 index 0000000000000000000000000000000000000000..effb81ec918baad01b420552b70e9dd5766da42c GIT binary patch literal 134144 zcmd4431DPJwKraU`}Xbb+nvtbbkd#7mduiHC!Nf`WWq4}h#IMZ~ARCn|^wC=$Nk@6^5Bx0B4M?_2)=z|^fe zb?VfqQ>UstQ>z@&xyyMetP-MJtyqyshoLw&nc((Y(MmxAu$jDYTi6vWiO;|!Gs?AE&@>Wry*`%3pHl?df0kbY$-z4Xny-tuOq zqhJ3 z{j)2`JEMQ0;Vwb})qjE?G4hulfpWdM==B~j%>Io*Ms!*lbj|*=lu<*^nZ1M2zCvr% zjAhJBEMt|SVjz1ga-keLH;LSGufS0+8CCsCWtT_QYo-0lQAW8B;8cDvqT-b#ICS?z zdayNKL38{>y3igqs~IS#UFbnrpdU3V{JWy_%@I-^0>TM`#TQ1|!d~|sG^Rb&4pb05}CKH-won4_#nPa><)!U`L zTIM?`Uc(#VBkH||j}Gq`K6Wz;L?I#&Wuh)~Lv#t`lu@|=;___vaOG2srX>1HMlnzU zU4&W1Ymq3(pW#8L%#d<`BPs{f5YTQ))Np(F!1yxN3#bEGj;dY(Sa06Lgktu(8_-1W zOhzl$kmgwu{Q#q9OZ4N6zD1&UG5S_S?dkz2R{1f~?ncCnn$%rm)l~?`&XqR{m`x~e zLDX`$A}nu1kS}j%bO(Y|6+Lgq-2)MJx0hduN3MJj!t%ig(nj@Fc(l8RAoN;6%BV3+ zln*7!VF+5;jYCCtVpufw$9Vs5bABZ(j~I3F^AfFT3n|9oTL{0on^@Mvis z?WsJbns}TgJTia)i+|kU(@RjyQ73+g4In{gp`o?s9pKI-B|?K03TIAG?`_ae}D;L6e;77kxRL#v)PgbUd&EXjS7# z+-HmLo6=&X<}8oN+6WDBl&WB6I>WXqufW6IiB7l#(^e$vz6Mcugh7!#qn?9s-wUU<~A%!oZF!HoZF7&!}MOyM~8PD zAG?VGkzlMsBuw}ssn3^fq=vijX+Pw6O~7ikcmhHAYl*w3;*pz- zWMfWjARc$eknB?`=8hwhwHzz&o`#sMo{0CMCJ3#9Zaic5r4SJ;g9hard-Ks7dppe= zd%LU~-P2jxG?HNrKkK?uv_O)CF+ zHIIEY2ev{Dx$Trev|1*YR{0D(24-31H{sc%)Q8lmlSUzuOT`C{QD)_8RMucw3xje< zt7yWlAfhTi1WKYwP3E+y%HIJ^&Y(e9qqPogQS*+~I#jnU<{LE#$%L^H^&|oSlt2R{ z0swkY10(_fnxg@J#5Yu)YOF=&0f^MaGK)1RT3*h?20yWs2`stDF}ZkPdFl%a_Jepw zKwKgK0>I zqeL{SXEI6@pXL5@j0zO}3~v!(I=VQN9hNk-LsuaY)L94s5&-~G(-;x~U@!pq5ns~h z1Oh|1L7rUom+=wvAZoM!FvGPm9%v#!pO z2mqS{fJ6Ym>aH;)0>IV)AQ1qt=xYp#9A;EuSlAv320{eV_5evD0Kh)br6d9XCOr+1 z2mr7eG(aMt`xeqAdWbG;3XLQYl!AGo0TKb=-~b>I0A3XUBm%%80YD-E92x*5a*k2O zlwo;9H>&3|O2iNohXwc&0fEBN72PW6ulBe-x?|%6gsDojj1xg z+}((Jp+X;i2#5; z&_w-6W2jsizmzP}N@z7hQZqdD(`e7GsjA|IA$n$a62+QR5ZC7NOMlZ0roFc#O7eM_dqSe1TJL8+X$Orj*Ddu1hC`Q zHu+lcl;e7BM1|Czw+F@FfykrD4wCsP{H+_VDH zI*RNJ9~sNZcu%2)meaD!`-P4q$5A3NUMhP3Nq|)o1PM!4O^_t?NqWBoR`o?}k$N?% z2k(K2I5EAub)1yE&oLgyi0zx}$$Yi$G1&g;aKwyl8{dru(m{EZDqvf{F$$|i%_hIC5f`(o-KykK zY&F~)bdrx^3EAN^vcuS=;&^?YG1YMqj3tpl`(y+>B-*p)&>;XCE{0qoKWPNpri6i< zmGvAum+G`imkwi(QvEQhO&fc@PdRg%)uS-M&bSd-y%WqSe*{sL%UXrks)8BM7gO=h zmCF+G&SDuIVJ85Qh~ZfBbnk>2R2RZz6ntQdgFzA>d>|3;&5xu4%raII8*Q-iBizeb zlO~2xV(>bv6Jsb2pj~)fP^DwfTxW7$b*7xu;Fp3rHD*v}GS6|E%uSk$#HRt5t(*b4diD&( zCu3S-aDSs;Ppj7;x}a+amnw$oHf4+kIV>6EFkUQ;_?ifbn~pgUR~G6-S0qv@ksnDX zbTg5QefMzZ%P+rdq5IKMTyqR{EZRL!bkFbR)%Q=DShmxPUiqne-}l^)&e@nQY%+qp z&65V&kzREF%a_l8=-wZ0O&1nP)`1=CwiqQuCpKOD{I|A@oN8O$Jtw#y2ZNzCc|kCC z470ck|5!l&Hb+a*(x`hkl+7O&;7$AI=L>I`d3(MzX<|Z1APR3=VTVE3mPrE>Kw{y7 zU){MgRagPg#3p7;Y#kk$r3|CAufQMEQMO-oOFcecti$K&o+&(?i>w~OE#HjR=@v}9 zVe@EXrM8XoEy%KD#g0n6G-A12r=#UhAS@MAhC~7&(TTz(-bWcp8&W}3!jYDEBI#m| z2vk|@h2MY|$P_+_n3yElcw5|s?Pg<3P7|e`9<_apl%JE1i#akL7jtCXg_UO~G$;`Q z6qpF%5qr9EqNsR&)&n;NII z>S|>!#HARo33Klt+d7i_48tjelWl-JDyg(|F zXbh{*Ft1b`gCr#y$hVHBWvS6d#vo1B6qQg9E-VVc;G&Mb3Op?qo@$>3rct{?!aEVV zM2!kKD&T0X4tBr9`NAr*UQ1YM(i>B4Y0!O-P~`ZD3Q0Y*^oWWHWS`XZJ_mb`u#7*YW@ZuA)ls7rn?mKNN6%||Fh#WO zh_#Q)Uo=+4|GuSNw+Ewg_RvSUwW*x<70fKQrOs2ybLK}|B zu#UsukAeOOL^6^B|4Mrq!9i z{(1JkU|(VysluK*c$1hREBz8`bG>K-!>=87MQNA&d9;~(kPZ7P(b&AyOVrVe^?n^e z8<&vZSSD8A04?4;YMU7|s1wN)Zb(4YVub3A5Xu3bwv19dS{g}=$QISIO7Xa_kfSXH zm6SaX*wuAJ%P=|gA{j0JHi#B?p)-26jE-YIm3&TYem9jH%G13Usd4uU2+^<3#(pF-8)Q%&7y8e<9soy)H0Okvht)XD^c&~ zcw%TXS%wW`cVffX?z9gar7A0NFc!+uS29ipj{5+D-a)gky$~xuC~>Q=ay8K1hmcY^ z7i*#|LG@w8vK>QzF~EB;jj6heOf@_$E%n@NouRL=aISDrTaN9svw0PF@*-vbQ5qjf zCH(mKNIKcbrPPtz;+TNRKpFvE5c(Q225-ZXl$*?9p$L|wc&Mf0C28`YsPKZ+7%MsP5$|UL(rw#(`DX7Ybri+~k^S@>FGFfe33zlIi{3rmfW%+ARMGNZYJHc#f0IwmEh->o`sd8a~^ZJ+M+and}^A&cOU!+gx49ncIl@ znp@c9w4&&&Y*rQeupgc0%=-rgGI^)X$p*!h)6vg628Q~S+8K+8U1O-NXDM zLbaUZ+UR*{+e_O@Jr92HK{XQXiCqXi%#R#z_MCRu!&^!{>wz?Kd}AK{Cd#dlIZOIZ zPc@jSnob@tKTTgR(=jQb_Z-FX7QF?DiEvs_Rh*y~5- zUgS!)4U?7jYQUOM%KbVhw?(2;L0GpRL5^(51QC{*9178&L;!4X$k(^aX50}6wqTTg zLf_`3PpQ6P8IuI|&cv)Aw*r@UWpGoF^Rv^K4eVHrx*3zXpqPq@%Em;ui)mO}=-ZK& zMexhv@0oM6W)OWj8&feoAy=NUie50dkVMwu~FtDs*tHkJthn#co9 zsIjr?H^8o|$pJNk0+tWdv}uzVJTvok&ixIlG~C}JbTMLWeF88A=l6p1EOEXS!ima# zb*;NXK@W5xDN*c+H2gg%WoS5~MeW<0$yg2M5AL*Z*eK(|WzB|5E~>vkAUFpbwcC00 zTl)+`acA_O3L%RQc#@-iejfAtfelv)u9q2Gol>542IYy{|Gs*!b}#hqVy^pb5T{XXSD%Ljyt|2Z zwUPI*3o`M*4j?$S2a6tdw!F84U98z3zT*9WhCfI+`*=HK%s(8;!xZA#JO)Xa5G*DV zg2luOK!TGoHBu(#!R#@OjOAgBkX}sPrpYOAgh>{s_1^`@y&WCZ@8MbgJ_6O-k!woj z!>L>oX08b{*MymG!pt{e=EImC=47^gTNsl# zWAS+jlA1Bc&(_xS=w!ralnMey)#ebRs&Ghyn%~MM(z$<$8dg^vuz@knQN1->pNH3A zLKV436_i@7h1{!iaVp4r8o$H*j0>T^$3t1WpT$G3+_)C{2WBmN`Q>xZ^`1f0sIhZ# zx;u{tijP8oE>2AfE{8O=(Fpz5(K3V#Ys(cLl zx!@9RTsD8WfE%WL$r)IdaUs{S{M@c4`Z`AFXPEieshFzLjM8s;j$M@FjuNts}e z>a<{fi^w}oP!ZT_H7JcK3lZnN0^~cS;VdoE&ll~e^q7bTI%wwX!jUT9Qdq~XARdm0 zMJ8+81Arv!^5n^BOeJmHwn*kjTF^6f>iXn#@J?`oY1P!SO+f|PpaFF(&K}eOD2GgB zLB*C|aZ%$2J;QSKEbJbgxHJGA%}A)Dp;H6>0&?P@AsqN5Veo}nDvhKHsS0LU|K$V~ z4-U=f*SH8`Ur)J3b1PF(!!o6E?rM>UsIjnVsLr~Y{AB|u_9(Dftjmok23U){FZZ$(X>Q(WK+6B zQyUviRsS1Q(p)ue;Mh;l$A?Lz3L6yH+HqN8=y<}bF#POnB9~1L^)ro~XvV?y0J;CO z{z5<$OmTEA%uU=XD*v93Ohd9+JDiOrXc_`MAbTm#j>x9N*;C330`3~^^yP{Ax%Ntz zX8k4D!9*B>VnSt41(>>u02W~Sbe-f_yp(Nqcz8d7jdan$iEyivDvi_uX#!f{kh>c7 zmY+dD5n<|;X_88!Z^2;WK79Y=%P%{YlPJSlB^qaFj3BK;j){?ooqF5w#X!R~uJRu= zR(*|R7Xl^gWQPv}ijm+Q%mUv?EO??P2@f`)$vq$ESAt|obr%z@muQ@2_m$6Kii;Px zY=ZkQ(xWZ8Z2QokHAR`D+mm_Sh3#6X6jd@e4OASTg7=>3cm(ak&>t)Ri4t(ybCc0* zhtn}|s-8BSc7L934=oo?yFa_g;SEHvy6G087KfvG4iYFqZIN9KN}RKObbQpW6t$vr z;7AO~5wg!Yx#2%j_Of2VrlUym!iHv6bBGK|GMtgm<_qh5{;>)%+tK+!0=mc5kE(?+ z?zcS;F7gfv)O(W?cDB>$9Q@3_I`X@qzps4G;~}EB?9^2_5K6;x4%R{a^jY%d)g{sj z68=yT;f-i+mD7bAOfKVOJO_e+ zvn#|i#4y*HJ9rZ&I?{IgsNh_40id+@{1>LdkRjJai|txSxOaj%d{wsK!KAL^|93O~ z>;^t_G(Idyo@(j}e$!lFPQy}Uj#!F(x3=ItjU--H<$4AiKyAR-S9%|6)W|{696cpA z(mhM>FpU~(AuMB#^zcRZ*N=2}zy1V&uD8^QpmBiEz*S|egDasj6PsdKW;wH&B0ZS` zOR!Z06wGTJaa?cvf)co~Y{pz0;~lt6jQfPvgVR~a$2KAQ^8N3eO95tC+({QoNyKn|ho5v=C z^Ib!gz$^ba*d{i4kD&t$cL_Tki*$HbiM;}Ml|SJl=KYip?2}>E;u-U>SCD=3U3|K| zXa;PE#eDSBKNpvG*)~xoMva7VgVMs`-lVV`kvxJUvmICSko!G zlwoNR#v(xgQ zWIp!=MYNB9nq{LV>_{7-P|a(Y++-TViQK+$u7WhjN@9)3)qcu#@AZ)Re^k%RIMeEZ z(4S(X@WeJzp4b}66DG0$y?~fH)lfV?<^0FV!Pw3I@m+!bl&C@f$T)x(U2poqKVB=A zuthfhc&oC;aWwnH{Od76%Jz4h`x=y3rlXD2Cnw7n0K3G~T&i3zu@3AM!zd$XA#jgH8g|X~iL8i)M?Q=9gwP zs=nq@;B^F25WC6uA-whhEUvIv2W)XE+_4RZCtB?wMRu61`dDvBaID15V>7F?V+YP` zQfl6vnn|B$fQxQv6}}uT7d=i{%}wJNz-f#oy72MVgEp6199+avacmcodRLyst~`qY zKtJYeC`mW<4kgant{XENG(c8X;x+BxDLu*I`N&-CeYWmp{?+mXVF*eKlXpGt*!DW3 z#`OUA#V`-rr=*QU=g6g;PW9R#VsFtr&|4?BpfxQ(Id#I0q|V;NGP!K5a0cEIj*V%m zzT%DHU<=|$ea*rh%VJ+a8cr-fVz{%gl;G_QJ%upRkg?&mAyclRiln7B#bVTk1T=vc z;@_sJ;bykhV2#y0g>RV)-G`iQ{Iei0RH`R(q&WxSb$7K*WT;yiQatZTuxiv8qA5zD zR*Itywww=xY&B9a{}JBQ|K87q29KVIA-{gTjM#vf zsHXb$w_`r2_F?Blzy1rb^5NJ2D<1Ic{5|836H`&Hw};j-q{>0o18u8O4$&X8_NEQu7%=9gAag>KOnt z>yCb-CQ0Aj)|B-xoB;?2zT-#E02(amGl2Q=f9?zbFA`2a10e8UJOiNCHk|>40J1p| zHJo||FwIVU9%RH$#MTGSbJ_B67Ux!oiw~b%efZ?+!+l|m2#wPMy&MtF0)YPs0|y>} z2HeNTvfqI76hYv@DT2SMO*ungt8~r(;0&PydzV-E_8IB=%aF%L$+3VfhXE-W!-h+V zkB#b`7bdCtSfQDXy~q|@sATtlY~_2$ZAi>>1D95`Hb32Y^#<=R&b z2fwZIc%gYcSQ3Ix@bm|{20{?ieA0La8e0SG4s{8C7Q;+~J>TG`_a%BL^*+0X6N^z3 ztVY-7xtCJP9}x{WT-I4}EEC_!>T*uj=h*Mbtn_1$@Vm9yWu1k~8b>O=%Nid4!B6Vx zjxT%!_2#o3QWrUNI&{9%QFmgC)^vpHVxBCNTmkD&><$PH2N@y7h1hVWAWm%3)=q3r zC@{WBb@p|c}|FB``RZhHoF>wmi0~$>K166#gX>6+CmErR0ndt zk*n7F!ZjxsxO2GM#d!n|+d2D0-Tg7I7%n#GxPvJ=39kdpNyA;HK_cwBw*Y63)=0X( ziPcy#Q$XObZw7C|MG9Q)v2nbWC~QSVFk*B?8yXcQ{ECwJR)SwqLicLDq7<$hqEl%E z2`51vm?8#miR(JxYjwt|(0Uc>f(Hb?PIBkuIKubamoAf%JVE8mu zT}<-;6nFO*PsNKB6QGwLnMM(>QNewH*2bV2+%IBVsSc{Q7dzPLQR=-n}y+{vdc@>Xp5#05SrE0uwC&{e7U2E7_QJthnXWj zD>#7(WFaI`1Rv!Xam*1XhLc`MgWqC^s?#Y<{MGO9iT-;p>S4@>e6hv*y8w0z;8qMM z@1+Jn7Nl`QGG#H^`-RCdZxZFCAEN41U3H`G=>Lm)4IHZ|b+FhLQE_EdH$v$CWI+Gi zI^@4f@*(49SC;=q`j~z>OuDs88K$R{G# zVjAqjc?R#aRevC;#mbZR??Z9J{bl zP^fuM#)mS2J??)A1uuq;j@ePGn2N&e!X`~5oA5;Ak z*4+?XugE7u*Zx*KzlE(d%~QDRQuzsA9s2ZQ!S`03VWevBqOI6MC=^Ff4MZp_>gVm$WHf zM(5e-#F@BHjL$*A_~04i`uotMDp})dpR8ewak6{mHc^%-hi*IsuQQgDdosEl?!)Ht z{0KE{s0PLg!Kj!Cei-@V5R7n{D1Ru33d5R$D-8Qwls^m%FmT7#kz|eIct&P0vDKMb z!3!of?v`4~pzzF1$mOT=xljsMdW6qib>`T=@YK!BSaBbW;ov#M7^3{xVEli?7z_K1 zy`h=0xMoZZUC2=#s&T!$9||R;KmG7u7s(2(b?mxCxa-(44PDdZeMY7#GJOIAZkV&# zJizfe)7W*bslU}zAItDl;Min%%S(6*hW8IHK`UVcJ9w=fOjV9W;6l^UJO04L#0QgW z8?LWm-m_B*z0fe&MUl02AFpi>G)kW&Q}KZu!NYq60^WcF8}{HG`Uc#l#tpb&eCr*E ze;;nXI^y);K3r10AAI6Y6YUn?B9aY;djTW|UX9)89n3LP?4q3ePUa&UBJ#8I4YMil zXJg&h#feq7d}MS?3EUd!^9@J+Ze!G@RA7Ga>M6a_Vh{P!;r)B@6TbX6)z{uj0Z<8IE9`Omdgtab{h_A7+PYnooF|7M-3DdLWnegKB zOnUgHCEzhHDNoC@UQONLS^+ZV(ytNiWkr1^U)Y{O@1^Q zQL69-sx1ilP2RsqTdQ+i=n9(m$;>eDd6TX5bqhVYep=!;0;A|>m;mgSS z1A`g)a^rlZv{oG2sgpqA&>5*CZloM%Pg{;z4S^~re zc3fEexZImy%A=gJhNwV<=ydnAl{SsaQJSQpH4oc?_p4 z_+o*4^g@Oher;rkFOuOZxyHy~v-=3lTD$|*DqCEm#vK}j6YIeDpJO~ZTfY@1zG}mp zG7Gen@Rf_J!ABd;xd_=MV^ly+yUf&EM_lZ}^edubgWwFTL+0V>IeDkuL9UZ`9|s@3 zHSF2!Y-bJoH5+f5biywpA4BQ)`}P5}J+22#9DCqwV$IDM3TE3 zB>5<6v37&`_h={|_%g|KBxk-x>6WY#l0v6jp!fhvkJBS<^xM85s7RA4jt6|`n)daM zR(Hu+fXmO>g+7x$Hz1+g;4}NrkXYm_%p?LOAIUCu7B!N^Y_GG}>D9{E=k!fcMrh)G z&VK7Z%1(0TI*ZXbY@ViSnd{7R=sTSuYY9p*dr4T$`n8%BoPMg=fHN>K;H%ljG=7q2 zu^xK8GbvgNu2MLKnH5bH9OW!dIlcJtA%ApHw;apkY+Jb(w8LXt&px^m8Q&@}PmL-<{ndP=|Ed=zjiC6M(O1)(6{L(4k3Y+EinIE0-6 z^`G{LGbjd0b}6L3%2~A@gB^`s+VEb7IDj7b8PwGfxBF5EXI6KKIq|! zP&x6&#PtP;AD??N3whX97LJ=9IY57nX4n}XKJ;&RuT)s_gt>|%Gm&ZHaHu?h0N>$&H;npQH6rfMdftpMOYujYti5g?itjuv9KE2 z!QFtf(U<2&sVa(l>a;qYjbe6=mHcZf&PLR|k;tKKL3Wd~Y49%mC=?1%xSO5L1Fun) z--3h6tV~@!rN|yw$@*r5cDya6<86P4w0D9z?sDvhj7jTKw6wYbV0Q%o*)4{%MUKzF z^H$z|akl#GZf$0FtIsZPAaGcs!RKkAZ3W|7SuD)>HfI}N$ZR;c!xlxp@jiP({Q=AC zaS%PELFZC-YPvtVGCa`0+y$-&FG{-;N=3gYe8l7kmt1IQv>blh23 z;)>=hUfb#{T6F2}?)=d64}W^29>cv(F9to9}Udy+G~DxzB7N! zX%Ef7-3Dsw7FD&J?B!I2IG)szwx0T&h^2U-Lo2k?Q`hDf3~HLp1=wR}uhi2c-q68# zf*Bc7F-(fC;|?#!q?pStDP#?2iRCQ0R?k|_65ua!mNet{JN<)?$^@R(`}J%lmn{@d z!gZm-wHnnah)$G0h#?^Bi3}1f>ltT2@9Q)88Il2mi@xq^e(%=Or#Z*~b-7NA2^^o? z$h8#aVuc4U1*g!=%Tj0Q;M@& z1*)5X$JM%WKBk%Or2pfD`j8z)LsGV#{1{5cHyXP9J;jZ^vz=BS1`k67#QfA;z4y4W zcU~p{&CmESu`9}(QE6W;yL#wOs;A!#tM$-a176H~yVrQa6Wk%j7Fp`OAI}p=L;KO9GN|=ub$oAH z7&O(VCVsPAy5|m1!8iQJiit5xd&h7oDmhk6){~g=o!GjRyb_E(6n|M#UchWsdFE8N zqNTWUhwE*Qxi;m0u}GuQZ`7(f!e|-1#?mN+yLLEJN~rCI;ydtMLwRx*G}TY}CLUd^ z`Yv5j_RWy5AZr7NtEG0sY8;shrk)vNc_Eb+oD;#nhySIlH5R56CZca`;A70#R>$^! zoE_80*%m$>h{GLh=%xEh{w-0hZ@Nbtj$g`fJuWEXcSq^V(BDwXnEnZbSi*Q~LONcU z#8+3uRF>~AVZW2u`#Dmo3A4BlO#CK`2tGPFm(jA^hMwx3V2-;IhUA`E%{aK_VpVal0vzVJtAOLKMgX_%9;X?1MFW>P?i%9u zBJj9J0e)LE{z~Fw-vipran};RailEw#BjX`+X=mFiC>|53+4iSW5&-5IUPco`huC> z9=W)LSMazw#9=gf(bti=f9ijrX@2vspzY)ES>MlzsT-iOyC~a?ID+6o(=Y4l*T40x zrcqm5x}iLpYkpp;@hpqyqQR58+Y<+Oh(+-JWAQSpRj(fSdz}ru9N1^G+ zCcH108{P@DPTL6ZqVYUj7x8xh;oU_(Jp}dQB|y0%K~I}GqOVH`1YF?FEqMNLmXP9Q z3S8IFn9X|w8y^!JIy_{HTYABd8~DyDx9<=x&Xq4h(|T(MLRQ&C?Uj7sC+lRf$9ow3 zN)oVn94YI(h^sBtmTqgQTbcO%I9iz$q|@T>N7GU<%=t9jq?i!J+O+T{hHS;LiaE&- z`7fi1dyjriGzRdLwl4kF^NVzxO#N5Tgohao!!Z#-%>HM3IhgT$6#+*j&p5Ezq%l5Ba=a}d2PJ8 zQSUO?+I(;0zP(v5dUWWshX~XI$0PXtddStbB}_?}# zv~equu5%{w5N9xc5>5(l{#-1B;2<*9+FLuA&sn@EXlJ34t=erAcV_8G!f^seQvCyC zQ~GhZ4Yq0U@s)5MKN|LWD2u+NYPpA?4t-ZNuH%ysaBo85jaQLEKM^q|H&3(exWwKb z*pxp*ho3@yK!-uz68i6NNZj9vnf0G!;{_VdrTzy$#OQ)w0Fw%a=D>V>6ND4L{~2$M zLGKF1nJAk2;F?frXu=QL7M%r*$PTLs$Txu+}A! zmPMy1wqBxoC_5p6*5Zd*Igf8f3r}TRqtV06=waHX9Fv%~B~uWfze7tingaoT{JzP= z+X;I^{|*9PR0b_WUgE?J34P>{D$I-Ob7WkLCTIdb=7`&lMf~gses+bIZpjOlY5jdb zGX|WQFZy5t>t~npI&@3av~oF20&+vC|DI^=Ffb(Ovv@;;Y78^oPk+TMi8Hbmczi>c3H1{jxiVFkm7ai0aBKAv29f<4VL5(ncL_zhJn z;k`L{@62&rX+*z>`>5{67{QNe=x;ETvSx74TCnw{%xT2*@!OczaO7h76b1L|*jp(M zQTQv#WW#NQ;`lP0*44(Z=j2v2%wEe|3bE2BhG~g4xtW+)V05$u~i4~j;)gh6n^Cl zRk=r??rG#ZJh*R7aY*lrxqufz<~T9RbVIdT=xs&wwG`PO zz<%A^U2ptOqkq|3o5UC^MvW}+J53op_j&pI8mI(Kx>@8TCs(%h+-j2V>G!{B5~5M{ z_eAI?M~x!Y`gJH*Q*FAUq%VSV?!hFdPHT+cy3c^52D?M5stvC|VPX04rik2;%Io(} z0xvzLD(&K$6@g=!lVSO2w9BX^K(d7Wk2X(blN+4w#oX(p>5bL|Ys$awABn36(5BO6 z-q9WDZ}@{d;p}JOYx@3P<6C3sG-CN1cr%Tk{_27*&UhQ$knMX5uBglbN`3e4?M0pR zCY5&gg34E6-)Wbdu1~B$IZx4-HsrE5wI{gjt;ZDgkAKwLGq!lVh{Wg}4|s5}>F8Du z?x^63Gxn;u0iH2&4)9Tm84iDq8Piz!9{Wb3Wa1v&f~MjB_K^qQjQ^dksC?J1EP7%d zhE?nXpo5N1saXDK)7pto6mR5#p# zyCCMlryJlR!11-Se5BAFFYI9kA48F1+{98&?^_zFb@PC}rcrN`?K07 zCw@GmMj4im1dYZWnizlQD0t^ZWUZSAJ(HHPM)5i0);xPFxHV6iG~LT*rjX@)mTl1F z&d8`ScSP4Zr5w9bGH~sla^*W=tQqr}saAr{Fl6q}gF?$i_u0@CgO~leOS3%Q6NTnr zi^lKgV2h^TCun*Ucd7==AptY9r#y%qS1&+y)Y!>8W7hihGCub#oqWYnlZ2~6*;hNN`eHN<9JEh$M z9|Qan=u<>{13(%>^ocGjw+y$f*tP*!&3yX5OQ}Fs8rv>%K#F9ZSb{GiI;J;{xT;uL zik0zliE=oKflpaIF&n7pQsG@1ivwJ(K<&-&Ky$XhP%$8dWAN{1__rE;*o9~CN40{2 zC0Dp7bEs={G08=_pWoNf!!vZBf!{wI>LWa=Qtm1LOo|e=c=pVQN>%u z(0ki!hf5Wop33nN^*Z?I@UndDW)_yQ1JKnv!MAzr#>5oRJexGvhG=%NxZ%y=BkJKp z!la2Gk^oI+f#yb`$;4i0VvcdD>q9gbu(;tZ;v?$e>s6%L!^duBfhL*i)4+1lLD_H-}@X$^~8&jZBdlP8s39rgYz^L&d ze0!z zV050v2Sz8T>xpx-jLzHGx*9VvI{66Aoh%v5ow~lt1{oO*UO6&Cyc;#L&2Rf!>WM*_ zZKe8lPukwDTimW&Agly)?-CB3e}G#bPjlHPdVFBPR#+tYyN)t$n#TuzlNovENS~e! z&ip>ToTbqdZzZ#w>AV_ZK~J26c&(+;6U>vI$VpG=KG@9yH9^&VK)_!6fOVq}JU-9| zZ^2V$$Su+bA0jt3W}*-H2=xI=Hu`||`F+sfQujeUZPduH-v@ot2U2~zZe+VBtP2l; z@E20}dqWDO-dO+s@Pg{&95q$eheOsiku?-auUE05K-M51T3S4a0Lu|N9O{E^uzjs3 zc$%!yG+?1sc1_EgfW69^bwk!@rL0Dfr(TWF&sdGLzB3cD<|8C)mTbtHTvc8PeRuaD zhQo$_bP}2HJ3zSBvS-w&k#{Y2^@OX}2;rq&H@4lAcCdr(`idcv-y2pS`M^$i$&Aq4 znrzZC}Ul6CJNj08+yOW46nC*Rh|*fDF|?5AeoH@12ii&>a^b?so^l z)4JnA;x-7E--h&@NfTQie$BRCp;04Zqno%J#42YGZWp70tQtx0B0eCj-ET*vE!t+f z+oYc+WK1x(#n{b9sGnFA#16;61#XL~T*zMX*Jl?FRA{r1Z`bW=_k?As!io87wMc$% zSb^jlI{&g2$R4|dt1!UitzC%#aCHEX2msdv0Eqx_Z2*u60M`Woi2(4v03ZJeFKq3HqAOJ`NfDZ-$i5wgJL^WJ{#0WMEK)fM9ln4MH3IGxT;KKnxB8M5(cR=bM zb3H-tfFJR9G|0g!s>Li30B#Nde#A%Rehurc z@;CZ68eb?_@hkDQ{2GxG zufs2EVDRGy4HDkvMDI>{Fv#ozULWMIj9HhRvhrK~`2KtZ(~DAjzymx`?*T>`{%crd z1p}*L8TK1K<}vsue9;{r6R~__RPO-{)uP(E^=E)Lc28EXK;d4)doLahL+UA`%exW~ zquchNb{MVDHQR%Jg&OBUyVxM;l#Tr;G|7em$M;gs#IQe?yj7u1Y~>Jlad1IRTF@rD z3jMI%?HEn~Kqu@fdfIk(0N}m?0mVbwd`;9&%cPFI7kb@p!7WjI<&$ro;d6F{A>8Uf zSL2g*C_y%Gxg9-iC+Rt~<(f-74(r&rfT17xjWrYWQSKI`2Jgcgm9fd(>%q1!BuDPK z^7X0NRALaDV*GD-0!#b_QQX_c<^iz_IXxtL{4(#`C=ni~fA4G1eA&;ew6j0ONx0PZUxQ53zG zMZeHgbbi!5h?yaOMpX5J=Lzsy{UUH}>;mq?)2Q8#5MRRm5+2nD5V!|7mE3C7h!=HV z1u%A*4Vn*YN#TIvDaf^?C4rze1uxW?|H zdG-)U^3~+8Adynhc$*;#2e0%DAj17E02Gw%;qT8bU4WO1WLt;vxH-oMvvzFmr56(L*(Ce>ZorZY6!bQHt za^FVaVQ@n&>V+$Rk{xj~ad8-hi|ZO$RBsR}Ry#dR0yv886XVq^2*;qjbNu; zYJH+JP0N<;F~skZ2)msc;JWb|>>D2b#!UW2Q(^T~T1ND{P)2magk?nUOl^!ZN{3|h zN|dY1s8OULqq$_URxj+!sDVjw_%dn+8Z{QxGQuWB66-M~d-!Qmvx4hh5E`Rp9OPP; zz}~|b+a--|vqFbN+~EiutB+OwF8gghiWPR?>j{jw-y;aWPim^ljY5_2@;YEw?rEe^ zwdH=F%<9brI)ZpUjUOOm@U|!Na5c7R&}0tBG=bUT3t(J&Q-AA`ogn+8q*0`y0gKt5TD`EZ0S(NM1~dcl1tb)$ zYk(wpnBny{D4s4NB|sl-aDYgK?Ske05ONkz7|$q=VK3<;1hy7dd>!2@T_+%IhIUdQ8M*r;U&+s^B zrB2F=82Zc$Lsq@5-q!BPRk%Y)p@?`^rO-r?mR-o^WM74!P!N7)cV&UT`y}Zvt^OR3 z2^#f{l>|!-w=P_AxLxVr`s#zXMie>_f0z%|S*w>WU%G1fsuh?gaOs@)dekGQD79!0 zzF~hVz6pZu)1p_uxpKId=f0cWm2HVzQ}Ob^4q3$;<9aFA=D zmMdbC+s#@oT*+|xQbJa)VEpD7!|%@hEWX@w>&DM6&!`7C9=NehRo4$~%&7782X4%% zuBDY_;Cb2ksW!EB-Oxr${V?`?%U$Yl%)V`^V_wSvmU>_TdHxT`1h=C5pIMSq*;Suk zpHurSX4%8LS>q^5S*l%l-YGI{mH50ATeN?YExI;N%=bw6?}DFSL=JyDkMN(Ynz!Ck zPtIewL)x1V3LlM=^82NwpAg8K=8%*AZiWXfV%Z}kcLclvBJnq_IDTtRtyx44|0dFY zw2!$_$Y-`Ht~wkvpB{W>Nt@cef~{V({e8nZb>o)GvWz-&1*P=zwhgP=)cd!$900iu z5pvf?Vjf_T&P(&3S&~s77xgYjEN)%7f_elmNPshMw~}cm!-~My{m7witKlI%WUK1Nf)LcVn63$lwr~rB00U zmqmiqj)hOJI~1vybc%hhnrP0GXvOUUkph9V)7llG?NVY*4W- zTWyily9Sw>hr9YLw_qVtz4)>;Q|Y-(_2WCkOx?DbsinBN#ne^vP8_~at-zfsrtU@! z7vc`YF_L<64lxga&A~iNXl_!Yk{VjhyzS}@l6qnmQ?FEINnJa{)M4;Xv)uKl{X%>b z_5w-0M)*7iYZdculln$5TQPO>dX^i-Z(B3<*jlD`skck&2g5q`4oNK)UUsRACH2%2 z=Dkr}CaI5$TuxD!OX}DH^TzSpnWP!n!PJ{@+l#3!^OmmNfz$gdBz2sm&Qb4`)M=7B zS6wM7XMmU&tE(jSYRS7?-6*MF&1K%TxUoZ;1CsY?b(^Hl-|rWzcc|OdrzExCCY|~@ zcJQ;*x3*89++FH9$$NZ|sn4nBRRn5yR^PLK-4$@^CC z#M-;ne_-#!+HcJ;^_Xe_ky+|-$@_2BC#i3{Ecc>1Ai~-oU-#y9JJieS6_WQ3q<*5Z zk)4v-?Y?>4vuaM{BuQN_c|DO+B=uEE4M#51dBFS=D864(4B(+Uaw=xBq4@rHu`>FSCWx2Z|FGg(0>_C_wEs+Nze~)C5 zS`5c?3%LBdl-ncazKT?I!7Oz=4l;MBZ$=^pQ+G*=eh^7Y>V8Q*8o}q_LFHmeJr=Zmp_%h%@@s6)06M;55hZaDy9IR48P z!oM%!tO3Sf+`#Y^Ylbmae!ua6d_-L%;eqXhtnFazcEJo47~#w{ieEZ1= z|Gexpgoh5i31PStb0g}6*=*Mrr0s9$JPVLw6R}$}%aEXLJ5cqnDe_rJLUkD@W$NBdmyg5giKiql)= z;V&07;zM&8A6h2i4u-naz9}4A_#|q%dHtsm{$ThHgpaMg8{w1N??sD_Z@&TI7o}e= zmtM$pvS*(a$PYU22WDv&+k5ew{HZ%Vj8!cR%~eT3L2^u&Sx_+T30O{*M) zcT0HUcGh^s07C1r^Z}8b6W4dgJ9kdB&0%=4#9x}tvVY&)f$-Z>>hdj&KR27<4}|9j zCDgee-;XtZXMi)(=T>)hM%1k>ozcYVh%!Gf4;q?+8F73Srv%RTSFWl8>s*=nVZTqqB*3Y4| z*Gc#rhw)u446lJ4EY&OUk#3bXRY&&s_*t<<`=1I5pYCP+CJDDg_af>B)EiNsb>9qr zz9?bNC1j^a{sR(fsh%M5TM%06ouI!!y?^CfA-7u*M${7U5K#vUCpQZZE$AIfy%$_T zV(qN!5{yJ>6xxd)?OQd0Qb$Lb`_mXhg+8^^xdQ*o7L|^uZ9^9VXX^sy4oUdDXh^$g z%aDYJ&1b*#NPJMj->$z5kY~2C)GbnbkM#R}Xmvz=+`SsPCE=Fs!d%zKw)}^*rFX^q zk^3jCFX$Z!?-WTM1}XKalLpD*WfFd2fRN9iRG(@=DacAYzndRIl(s*eEv&9<8n z-r06L!r_Ii@v0Sr2v5MeY^hV%t%OExH}65c=bQI9%{Y7J-G@@dnF?PE$RDuE2NX&s z=Zdr9pPq&FJ&|!D>LW>pzfN``Jl}k7Rx^dne$5m}=NbbZfwqUFM#eSI8m?P06aUx1 zh|>J;h?9qpCGM0O_tqBTuN7`Di0=(E%-jDFe1Dc3NNvsUoLDX4VhNwO3Hh~z&mxSd zw|9IUeNo2x7*ThL-kvOW$kh`5LBfGfuB3mG)%<6&zPnv)`}-u^(e-Uokab5x&KLMY z*~b7c$=alIw~KZ00oWD6JiH}!G5GNi29N_&?*rs;36m0<2p6bB`#IA-h1q+7I;e%= z2iNA?BI->;i`o{b!-faiEOnsGHRV+j9xCBq#VR;R;#rHi&&L_&CH|tsACWPB5@sQ+ zuwtkEVis{6S>%6!SLiH!><^>?Ih+0qdE2O>I#G>S#gt)+s&Nnh+J=?MO)8{ z!k;DK3&L%?aIO;MaIVCEfcY8oVK*VuCBU|m=3h|bC#@F|zE-$uGs(k(_=_B;TU)v~ zBIedZL)N}8%WUxF<(vmPvj;?SYWJK4U8FYH6;Qh}LOOR#8@~ejGD1OTW(pX^n5iuF zda-Xmz__ogmv7sJwfGZ#3}Y_C#XEL^8s~G^(wLDfwL-$@WJcx6i@H`3zIEls7`a*p zu8xmvKzONjAi}HED8jR&rz6~EF|-org17LvZtJ^3T76BI9;Y+sUhn5d)XDJ=BOXb9 z4B^$uPwj=;XY2T{jo*#9%yoWCXQjT5)?F>(q|Nx75Ra(&vpLFENJtJND!ZAZ@~y4X z(sf5dyN)sme|7u-%(ipv_V%3mbM&Qb4sSxdqWuW$yOg@9vjm@IM2$%5VbwMC1=N4G zpBk9EIBuwO{L~|Tcfreco}_fSsCs8T^*~oteWafH3R0h~ry}sjJ*ZRa>)qc#-p~Ei z+qOQ|l~jMNW71P+Vwb7Z)#}P6zvxPfMt1Rpj6ef9P`5PyAHt z%ICYD}PZgz*d*-woll~;e0)J1CH%9hz( z%3P$&{bob=Y@QwQ@8KBhqgS7Q56772A2lJx+Ue!z-@`H1K3^x0V(s1a6l?FPr&#+! zb%fN%);`^}SlunD^VG@k^7g6!#oL#_M_pa}-+L#?Ou{5&A*f_wkR|LSA?yJ%*&vXx zC9H~Jk{ObbNhZ!rAaSWlLEN`jm-@sOp3rKm)mAJjR-jU=6Uw z|M#4Ge>0N>?dQLtzcXjK%enWSd+z%C<;gJ=xL0&DRXdg_T_VT~Mfnwo$>X$)^c*j` z3`KfQ5JwF~dQKEBoV`<;@8_puuX6`eZ%fi_viOamNVBN|e=Gq1c8ZU43q}-*Lf*0M z6@SQ|I%2wbmMPsvGsH`pa*RnofIZUVhAK)=2lb|*cDoLUnc}|<^^gm@5aMG)ZJBxi z?~wn)P`y*rLGg=!VyB}8@n#Drc59%EqYLq7i)>A?CC?H0QL1Fb9I-Y^VJ;&)CLU=v zPxuW*n#~g-Ly=~s;!;DAW~E}gp-8j&;(9}oX7j~uh8kZwf5ZZ@%TRMlD?sft)aQt| zP&{a;v}x&}er%}1IZH;AiKh%zHD@)bpBrjRif4pd;EE%c_Og@!sNWdsk(90x<>I8F zo=({g>WrqiCMrZ0_NJ^JULlJ6Q1dk9cw)#QXN9OR)ZrmFkEjr}hFax3dhmn#g&?3%@&DkqSO+{BJtfQg&hlV zun%=qQ;x?g4nTIyP`{{12lbkv7K}ZBzs>iCp;nGf2lck5IKNu)fsws6N$Z>YItIGYj^4Yjx|9n^F~ zO`LE*tPt}IHD^LPs7g(-W-G-yBYO(DuN0dM^*nN4Dg1`|$&C9)tP+

ctuRL0xL7 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 CNdjE} literal 0 HcmV?d00001 diff --git a/.task8-sweep/ref/LessokajiWeaverUtilities.dll b/.task8-sweep/ref/LessokajiWeaverUtilities.dll new file mode 100644 index 0000000000000000000000000000000000000000..7ca4e8fd2e1804f92daa5e6d85a58c931dee6cfd GIT binary patch literal 10240 zcmeHM4Qw38b$+wA_rsHTCXf1~Y09S`N(@PzNs+dGtjMHzq{K+1en|b;Rx9n@4apUc zx2N4bNwh4PR^k?J?AS&SCvly)a*R4g+}1#Y#4uX7sT&*lk%1;Inz)T%)J>5#F;p8x zVK;!(@6GQ0kaP^EDNqFPlKW=fn>TOXd-LYa?A-7j4^x1MbiCKD5j~HRpI!w&7|fzM zcH;{%`g+Z!RnLn(msX8Unogo%TNAc1oyZvZyyYg+GGQ0<2{WHa?Hx@_TUnW`t&Ohr zMeiFR+9LvV>ag)rX|y+Jbz+g|Bw7hd#G{_rhBAS-7cWtrs%xI!Okn-hGYbehKLI*^ zAG7j*rSwrI;aLW`d%4m}bhIpl3I21OXfdiCw-Sw&Xdk9blvP*U5#YP3`Xya?#syxu z0RU~vR$p&m>PZkCPuh;10j6~8MJ0qcfw$`Eh4x8X<}5I>u5=tP%UXxG>ggrgQH84N zC)lreST~J=^cp7S$TA|KsJ}i~skOq}(YCE#sRhnKlHNv5ojRo<4p{y907%hgi`w^x z!jCO>>cL;DMXWf=mJLldSPGie0L)&5YU>izoTUJF)mYFhT4OB-Y+V7c?sBxIBUEEG zqW(|eKGTMApoKVs#Yw(gs~y4vg~Fs$F)iZ$R{UmsHDqo}ffaeb&G43hEy z#<&se8$Nq^alH<*?vddR60LLnq0xP`>yj5P8aZ_GU%WczF98v4FRpkHrD$1ZzX@fY zY*}vC2$Zwzy5*&&ZbjnRY-{Qa(MuQyNWEsQf{2znn#F?Boz*~QgQ&HxLCr#dMKSrC z9|AYef5d75(5WtLl7F>ZTp&5~A_5$))xvG2stH?FWg z2DCx1*O#=y$Y@6cYRex)=sSb(dQjg0=Oh-vZtDirE>kIb zZkxPz?OL0PAEG3sAEuTj$ZJ6Sd~22~`>Jh#14B+H zcv@z;r4@bK7lk`Ed;J}V4Yh>N!czSlqC`g>ldYcvo$H7e{T$**Z&_@0K}e*{>xaQG zetVg^QnR|z+;+(RHY}Ux^0v}^meysdT&D%})*kK$%FU|3a|?=DEHwQ*18WmglU{u@ z*AHm9^IK5K-3fE>^gh&kM_Z^Zx@5s9TeZe>2vWVA3>wd2xt7q94wW3i3xS2G)KMyv z4%`u%<#;F$Fc`45LIeBnVQMQ`Yt8+z>T2)yT=}$ub9rOc0OTpWNOm`LMh^lu5xz}c5IH_P- z{~mZ=QGM@J@^>iy+mwWF5KGu3a)7spBjFG|6=e8J#rca6x6TNLBdXW-@QunH4=Z1& z8AV!uOlaXg_zfNcHUZBtrs1ulw(!lNVblY3FWSCDjOi3sZ5M)!g=mq&9uSX(`hYD~ z*iQqDEx`=2l*fZjp%5*l7%-w&sR{j-g3fJ&fxE!D41bLMqhM?~{up~jVJm2b!qzFy zMrwpUb+i#d(uex>3i}}|ZME%0@;Ja|DY zqxVAh0bU@6t>S)BPxpl%5QqZ(e!zDWyd})_e+x1EVvyltaE9p5iJ^!vJfrAY1;48J zpHh8KEBKt^{4(nGq(>eF|J~uo04JbB31*bc-&OEdCFci9&O*vf;JJk!pf5vbK~DlU z(bvSj1RM8zD_#upY#u3NL12>#dz9{`uM0sx^Rc&p!JDhm&F>eW6$*P~#oq>B5CPhy zut({O)Ci1s9HK|*h470)r@!&BXX)D_NZ(bM*DpkGD9qC(L_eE@{l^^aT@Ryo=v8<^ zTU*g3Lf)~~lNq6eYI~HL!>@@5b@pue4=VPA>H;NkS_p!6VpNJ?8D$LU*N+WZy zLvt`W2Xp6OcbBm`9DzPv#spTtXXjvls4!3KMfCKXwm(zYCn4cq#UlEakFBL&2+pkh zyd1nP@{Zu##?Q;aH^QG+na;!Bp?6iApvyXEI1k>$i{D<^!Tk!}uArfyso+TkX8;8~0=Sg! zRrO~Syhu;$?R1e2i7pc23*npT$Dr5Id$cpSjh>-!WCvQ`qMcMDK7|;)NH2v9S|x0e zLne7H`1@GTFT)qy_i^eH?}nd3RH|4Pj|97Dt*UPk!Qfxh7Ev2|84^y0zE2&XzYaNH z3%^0%p-qwZ0RIb8{tor17!s>QT>O}>gI4>+q-Ydt=_Y->xLy1x)G2yY-?j9rz8jRR zHY&D3(&v%o-l5%?F&_PGAHE*udSiG(yiF@2Q{p?mrR|Zt5F)3$Vm-y_;Xo4ca#bX; zlLfRS*6m`zHPjB+PCbC#bT{Cw^l89;`U2o?dKPe$E&<+2uLEW%202rJ{Wxo075#{( zt0JT78NYs(Is#u&W4%ddX*58HH}HtFG#&UPU_kqdf^Pzz3artXr(a`ft7(M3N-ISZ z-9r!4H?i0B3q$C$968m`5qvu#)s~+bwTTd2hdrVWJ8(VTINk>A3;e93Kc!7{7rqbw zlaA4Lu@|qTqn^-h+qWD$)_ts#w(YTu>`>R{k?m=pz%4g(%TDR0tW0rQ=3Q=N+F&uC zIo?G>19^~bBb}3s4I5{6OIeV1Rg-Hmrua8f%FMW?l{f5};~R+w^dO&DHWE~!ZNpZ! zn3LO8BWpvWGma~#lS6wk6JwMy=!8N2MaQ+K`&^uL(nVLM4A(&K42_wY5`Vvy%Sknu zliVru(nb>xNPXEX!JuL3PFh*YOV@FYeAckDl$A6g-DAVjaf}H`paTx&PgzqET{A{b z6248js4|;|Ql?X|9CiWqTY1Mq$zleJx!i~`Eh*37ux+|>kC~UWcYJK7Ajunlhv7&# zg#|z=%q`nQ1Jh}l&B|&&zmqBIOEE^ zgGi`wm4+LHiHD^9{*6QVam$`IR1D=7aFv2|%r^2)PH`=ik2xAQCyMs3XiiBdW19sY zq0;w&EaZ%gJZ!p?{v@y>mG*LNC_jlvbNeS{W=h&+G98fPqsF*&XAU5^oeJHPrc^l~ zbH*7}t7>OAY;2Va7HmCaaoCUZ^k+8xv-x-*Bp!D*dMmMt-Kkd!U$i z&1uPsL7B9fGu^7zF?$BHdC15WC7n{xU!H!qq(3JJ4D!B2P*{c2hMSp0&9GgEok)`= zO`9x%_F%PHQ^pDNury9d`=HAsn$k&Tb2)Mf5-^`r3)aY+qPb8N6)X=$tKltoimpCw_)0w>9^X{X^LSO_^&`O{0B zG-mCwPJ=vd6SCPp!?erU2APx{;ip{2k{J)G!k>whX-woT$2BufX~fdxloH6Ov`?8C z>G;bYlZ7M!6vF+uzy!>nnPhot=C_m^J_HMo!TE2Ycon4OI9$+P8Du`=1SWl6eo>|@ zpzSd8oUsOT2ELE|^yMHvGPdPd<8HDaPUy=Uxf#cFR7?!yjDjPxn1?Bc2J*;42ypv=Mm-ePlU=stU>C&KS zJMLaPCC80o&SgiIdQ=B6_e5@Wz17BCqtgbqLM;AiD?hQroR~nJhOJX_gs*PxA~~lK zb$zyN%%})Zl|fscEXsUlhDw=&tirK@lSP9Q0@sJ~;P5a$WYqNG6f{iB^e!fyQa*nnfKqG9TOHOR8A74>BxR#-q3u z&h;N8kFED5HjJXDi*YRU=Mg2Y`UfDBrP|Q!H2iBSdIEl9UiKo=8rpJIy%LI=fDE1i z2`kcQXKN|AmEm<+P{+|;1Rb~f>lfNGL|wQk8G&{qz8}<86Fq;S=0dR8nEHmXHFf>= zev<<5n4Sv4uqD*e@BbeM#6e?EE0+y7C)yas2Nu% zBEG*l7!QX%-lg%|MSL6n4n`BSG(LO3h|fM)TPK97ucY|w=kzEz!d~092y@*oa1!=< zLTCf-QFI=Lhty)>P;-2LB)&f`<7Rw1!n(zccrPw&;m3VhWKvf3$m6V423V1t!&kc5aZ`_EGh^b-N=2$IS zqENp!ihD+H4lz?OR>2f|sew?3vbsPOkJ%@4_EHwK3TV39%*R!zCY?bH)sxuj{!>qSPk=)|jB0^05Lx zB?$`Sa3%Su4L4FOQ#vyxHg;~r-El%tQxSQdf7`>BoH&h>2=)p70T7!c;+rLO;qDcG zMEJ2FsgU+;CeyVgyGeF$$#!qv+}X1w+tV{{q%)fgBh#~~+emNj=`^Iot#q@X6=jX) z9dz)0<~cxvk81mJIqUQ=^1E5Ucf^=hLXoX^G{1Q`!lxO{(>Ma)rp zCePRW35nr-?0F0CEH0sEaaCV>SPb9!uE%=?ZC9$=n1}C@tDgg-sZrVX^M_vh`A_@5 zdF6$VztS~)k!7T|9>ouTN9P%Tbl?ns@nnx$=@Uo&xj9<(DTYa$-=(tu7qV#@-PO18 z=3B@g@rRrI5n$Mh0nQ#7FaUWs-&mQ1gY{LCD&CXTCTOL@%3+T+Z3-6I$03C1X(UBnN z5bhq10S?ds)P``AG6H-EaM1hJo&Hk4a@AXe9QFKoI@ps9wX<8myyrRnIHzmqgoB*F=P#7BP4RS6>8}U-K72s? zaI+_H*Ja@(P&(1@ZjKWn&H-73IiDgDkUa^U&lwULobty>Pyu7|nZ!Fe5%u5(tR%0X zbe<`n5?J4brwPI@GmYp|(mADMWt1(2IkqnBd)U`Tg8QK2%|SCh`LI6xbYlOh$UX$R zjTRF+d;aUfDWtff;;$JP@lv9`#sf5b>B%Q`277p@ALb; zn@o4rsZ*y;Rh>F@YVDVd+^8~2sVx4Ue_pBk@aAuwJa_&WLvUx${hjKb;@5V&ukFaM z?RMOmXN~rr*YIB1IOUw)(@r_}TYr*mgbuP@~?mUDg`)y z8MW~$qVj+I4!i{6_wOHDsa{G4=w8W!NdKN(soDX%_T~Gi^U`5E^Y5ZW01)1ocV_fiGIi#q+BHC zWaWaa8s);+yHYMBw`=7>S+GH(tdC@V7xxhR$D%3cB(%VPoFISI2C!l~5QMe8=W z2LUVcjtmD8h`%_(&kS3c;RtDGae5I4s)p$^4wAg*zztg@kQm8D;s;hyU2kt0z zSE*!M{vmj)Ol_jAobMm7oAhow6py}v+Tv|!S)~d4a6CtkAZ&~wZktl|_WF8_oT&}F z76A5yeI)T-g2%R_h#V7uk}8cJP1qPi+>8)V1r!qV%vKD2L;i)-wqwKSFN0&kJ`T^( zmlHO|5Eo@#cTuQyqufx_sz@4L%F4R002)hPxkGSuwPn&EJMRu7q&X5IPFU@AGE`ZX zQ|L-iCZMIP<_0+Ra|G`b#K-4I*=n9BK0Zg#JM<8!2=D?2fBE?Q$(mRzh0$w*)r zPpAxyY$@8q`QgIQ7WZUC-Ry6~Gm56u3!-UOB)Q>{Em|_vGY^K2weC`gfqRNjIn|`1 zBT9N<;zCjqA41~8Vd5+fCtJzYm6KyxWYm7(zs#~gM(+8XwW4Ivao|K2%By7eXRUI5 zzO_XI1DT6tc4T;SxvuiiRXO!e(1CtO9arCE)!wwS#CP|bCM->-LxtIRuw3_nfMDR@ z9pJPV`d2D=03Y5I;Hq^SG?k$UfEXjvXrBDZ%Gqm&Hk9kjbR-Z%gDA(Cv0wn|m5cWtx)L@HERkU3>W&9zn3_N3e-KGQ1^{(X85th67!JGMF*Z z1|AACD%rs!0km8j(p;OcsuOLMoRjM>SP0oW3Mo~~-S)`!^C3~AXCT>jYundtm6aCu zC>#^^ojGNXz7j!wV+`@mgi}>3cKrlZ&*p2x<$U+vdlvHD!`@j4DF7l4%dzF{)*w1j zAxvbz2kNDgck%-d74m(xp>l}nolVNhv1>yW+p+D5Y^C572Hs?M4_AtL088}~!z7*3 zigJLnm^&5YR)Ts$L|qZp#Lt z!vTKi-9Z0s^xZZ*GL%7~WSwk(UfH05(khfyp*GZB2suGGYI$|z`RAXvQAa3EX(Jg_ zc*L_v1UVl34zhyeU(f2-!lqvgoTP~U-+5lUqAoz4}9l<_WDAJcIeD~*IOfSj;(#oU%tM6=TlJIo03TbCKhjBIe{pn=5wx~0$&(lkd~hCaOGv%*(l!5bg^ z&hmDs9KzNMKZOOm{rqFcx7X(`7`*Gg!>y48M?7%ZQ6>+Y!+s~D=7A>412h93Kt=`W zBg{xYqpn2|Iw;_J=O8CRcQAIwx(y4v2M#*!@4FS(p~LddWti=HcxAL_WXN_g7=ZVD zhEYdb+8M$n4$*WhjW)EoU8j_h^oBa}g`x}5j@w*Dy*|m4#vFbDtPO`OglSIB8qiGE zhLfxfr&*Kwy&Js2s&$m+O6O&`WN)#hKbxfMLh0?WWXrlRb@>i&$qPRtx=3|t63Aj6 zLBS=EH-!OdY-S1A9fhK87A*Lt-8LKVP(CT1ncOh`j|{n^h!*uR8TBafC7(?U(O|Oh ziC)NRUI<4n71xf^}mL04z`~ERe5ivx9^Bvv~o3kT$c*`wbNoZ z!c_0jadwk2qJEy`8n(Gaxh{$u9Ls;^4)9`~uq!wj)fdsp3~xyp>$?0fKtQ83>An+j zkUrXaP{PlwUC|l29}I0M+G5)x@LE{3h5o(6I$0M*jWL4J1$4~FCih|#IqjwWb`~Qx zw4xXh`fyOD1yzsoh_*Gh)`HdpM5W&&L-J4Olx2K5kg6iPrCho0DZ4%2VHJu)-5YL0 z&%P8r)NM{yx-Bu1x|LPDtzuh6!0)gMj(rCVgbo-V>}yq_h;D@j%)>gGMLVS*VZ-g& zg0{3^O8`)o2qk6b5uxOi2Cjj)iO$#yZx}`>_uhXT_>TiedO!Ep rzP^DOrg)Jw^|6i?f0;#tmX2C{o4W2kZFugyipsu!y{XK^jT63Tb&)<1u;-9_9QP@icbOeS=P$B#5>QI*66kwVsU5^6f4@Ifwusrcc+lz8anLHD2_#M^DS!I*~#|=m3 z3paHryLiesN>ym10cF=tu1;h`6LrCJ+BZseZ`&P80YTMa`V#^oS&WG=7?=;`f-Z94 zVLcMa-*DF6AG9lZ*#7w?i!t%xBU{-etoxk$7}AHa&$=h{hO_1kUH?b$RBS|DX%Lcc7vu$wSH^=VAbFMuL&v`7pzWFS@zMf*90X?NW zLwd?{@aQZsyk}ZL5T+Ld!6^trmm`sTDvpjmgNgObWV!V9R9R4cJ+-35=q^f(SwW21 zL5w*PW3H2xDDxmTj5FWKN~9hqE3tZmSi1$W7D%jxPEKMia&i)@&&f%w#ZFFQ?HUX3tdIlURj-I7KtUV=GrC>RQsIIbVw;k6jOFFJt;86}9 z!MoR}+n)AL2zDgbHOO?2bGxqLGNl)7svFXqPYCYpKw_WLb>7Yy4j|5T>>>33l<;6) zf{5(=pVBhXQLPl}6=nC7oWfAiDG(D@0##5qFkuJ`{G^Jhiy1&^cS=K@S*Jvt5+^jA z5?KWrrzXZDiPdctQq8i6WL7g)nqvrF?=!%mxIdl{zOt1M$`egS8V8!clodLEPPd`` z*D^glC!b0^qpc){+v+a}haz=GKZsc$7%p@DCt%Vxeh-yb#?(j#X2rp(@h5t#9k83S z=hlj(bN0#Av!Ss#H8Dfwx1o#47QMHlaf^n8swvinI*L8hgF^n2jBVM8^gf+IBb~56 z#95|05uw`n3#j)jcc)uuyd3Tljk7Y)as5ZAOw~OQG5XsxHF4;v6d4p0l|4`RjgX1~ z5@(!fqajB<(=>|8+fC-}@zHl6Rg4}u7>G0s%tX}YHRi{j`HiKqr>EH1&+wHRM;X3y;{?Ok z-nhW;1SZXNzGSfq7EZGhK5a=& zQ;iRTOkl9p`+~q~w;`TZvjg~#-(IbH9T{aWrY_DZbpC1)WpKWrmh`K1+Nw^V`?_AoH4Seu%GJXTDfMV8^k8XUL zGII&iJ7!i{4SpqyGUQBVrNimy?+;3Tr!rJUv|A+F?1{F{+{Q|$)7if$Or{s+Jfw(6_UK_LY&TBsdcnC;%p@|j+ks~wIr zy;5;1l`f~Ncfm1AE%V`+zBe2LaP%%zYRv@@R7g-)@6q(#9r}p0W+{Aw&*8yX2Ow4t zV=-ip2oK;j&w`P`#{#?q0k|OyW}<73fNzkL5!=S}%(8<>@BDz?^wWTHc^HAH+Z@ZyPU*k0cnijJtZ52EnPKufW~l8vTN=GISCtkEqqUs_(BBh zs(m!7+QH5U2=yM+_uyZFt}*Rw1pH|PoE-zd9s!re!22WMAu$l@IG}k_47@P{HX>ks zn@;+*F=UH|yfcQ3Xvn=WA z-mZ;OaWi_SABzIM3}G{R*B__U@WB}{{%ibw4}V|6p9D?c7zUjL5b8uarojrUWrmi5 z9yoeg`C|x`NUYs8KGW8^ARyHF6W^L z7?$&yF(fGGOJhh-&aaIjK{;=aAwfCc6+?n@zCVTp<@`ttQFF?welg5VsFbQx?O(2P z{iTfPAieHXb87tsHMdj)UOPlbhJVL&mpOW;W>EFjF!T_++v)D#Q-x8fqGma>Fd~I= z1-20vl)|_f_aoG#8TX@jVTi$(dk-EXm3(L6(1EOzujhmr+AGwg4|=l zj`#MynhlcXZd)wLCMY-j;j3 z1i{Q=*9JPwU}T%c)C0R3@Xj@=jJh4XyWoBFZrYx9)_)4-Szvl#H^EQg<2HzogHwM# z%E)T+8P0>y-8**Wd=*Zt5D-nQ&=XCp=#cA~eN6-NCoe@w8uKTxNIyc06bi32`UyN- zGz(0ta14Jp_}qhK+_OR(Ffv_I9=r1qBR{3b6WGIDn7otJDXgjYl(Dq26 zMbYyfh?aU*AO;NwE0`O3T-9?m7z^*H7cpN^7#zilS6-$%I9@uMayy1rJ za>H4q6PgX;~SKMjPkJzL)G_I!DFV00sn zO4hP_=8DtD=$yV@?VGE8i?k23Ala@QY%f?DA4X9-IrkWv9(l{jH^E}b?BC-e-BbO2*)ZGc9E6dI9%HjLGX zElxJrwIDk^BC>*FHAygtJUqfmkj5kA^$apc8q*hif7+MVA`Cj-#$JVzliQ*hFhz2T#vInDxWS!;BV`B+S6IOH1b!8q8*+!bVw&rI)FAJ#Ie zUayRgCv1>w)l;0${iB5nWVtrZMzcm^mwv!%GC@%y;&(rfBpdfd9fH+l@)$taU2X0c z5QfCs2X6qaU7<4`(aB&=(`qtT{fnibtS0ylMijZOoH?Sn40E;|qEiK8+BPT*%_#H` zSg>>rlfo|KCYuktH_ZWiTMinrfm4<`)xLPM8m`0N;QOINf>M#hiakd|xv~*g19(wS zabrH2`B8(IP*%lRK;kk_8mT)#I-W>#frMF}ek>?Sc0x+CG!m3`kXtplIASA2ifgK; z3_2K3t@-wasu);sG+3#f$uK*RNL(9jPNp@pgqPiw_1Deypu7l&RWhzYnf(Ws3Es1Eh#0oZe zOO&^&FVWn&uzc#ss>MOs4g{fYLEX!a7YHGsth6>`ULtJ<)w+w? zOp2h%@(31`0&PYLR+FKTg32JR9R4V`XX0-Vl?WM%;}(0)M4Jnh6XBt@s7X<)K}^mn z%*g_a+KGzC#Ol^qTJbPxHTZc%NFwR<#Lnym9VoC>=L3m~$H_mJg-SS`f^d7PTl9A5?_J2`H@=jarZ{OQqE!<{4HW+Nu^+s25Af z#n&KOPvCC{da{*^&?@s?wcJ%q-0dF`Y|W;&#t%4dK`;HqEqa!{e$yJ?+1E$lKhc{ zsq-g#nM{J{9ITmaLDN{tJXK4BC$O2!2{bVljIbeazU-D+sTxnC;&nDH{-n`KbH)BPHvH*Su| zH$DufCg6UVLEmDKwB3AFwiceN@}fyGm=gxDc9{1b1?u9)Gf`ZbI&rewo^JLox)yqi>8M?kic>*0%plVttNS4+s`R z+_Q+aZPaVnfn1gd` zcW0D>5QA6uN93}a^`6ALWJi_USWyavLhF`$g?f|nZF@h4r=0hGf)_MldtueR=pb>d z$%CMkItE`2U(tizR=NP~19NtxpNHgf4#@KV1nK3PVVg=Jb{2akFu|)^DW;>Z&$cNXzw24^7Vj4zS3xJqmg z>o;uiehD`!iDBNa;Ks2Jjtu)T5GSna{W?Z)e*-sW+nQ|XPPsNIC5Ax}?zC?ZVWsZU!YZ9KL=bjiX>|hz-uTSog%I5~u?-7OCER6M z-^zg?wLDfqMcIpI4$2$0p_C>2wn*e!9X%6h&T$q3vHDLWjCv5k2;5Rv+N~uP4Q704 zWu|JpSGcE2aAumi?n%In#)me|esw*RGh5s7_m!&~Iu{bqUcfT_k;>{)?)mVRX$8q-)V`|m zXHYY3rBXSxm2@92_3wR(wQS+&Ujy;Uu^fSwvp56WkvZCC~J2o^9E+Jl3`rfTpgc&z3hA?b3$nfoTPGugonJY|3lXY*uWGd$0BRpF!26@fYImhaQ})20yYK#e@h3P z8wC749dHE#svz}W>3~}iP(#2!(gE)dNKK>zVCxB2&!+<(2?A7Girw!90d47k-vj|x zIsiMcB@Gg2taqA3|JNfpx;Oa91;ZB>3|c0fP6Zj5d;*{0oMfq#dN?sgMd;x z-~j}%*vsjF$AWj)7vl7EKz9(}qyw;siP1nT z@+Uzq8!G{nGY%BRu5^_B0$SK?kzmW40RZ=xQ>xYIO!(L~cy!bkb!FUcxY&&} zk3ve`Ec&=)Tg1U8Z#H}iIM^g!8u;e;w64rV6`lH=p#L`cJb`>32~I@0aLS$g-_{x^ zjbjmNZ^LGDeKdX^9253@JV$#78)JxDg~cNSai0`zuNOdebleyB5`&uIY;FW*_tM?% zZM%^UkKJq93*a5a)*sMj2oL$zF-7NZ0Dc{#+o+T^Xo;RXKP=i`WU4LGhRu|dv}I;smTdB>@RKG9a+>$ragYVmU z@F!Y=69xGQf|LBQ3r#}4+5%yhn8bOUkVFkX3<$RKB++heiTJ%1$PZc|J1E3+&3X`> z!tK#6uCgdCxzh|2BOwY))>e4V2)3i%%YZ9fc+&^habgoo+Q#J=ouk~Q&ajek!U24CoJ-|DYZ`9Ur|$UUW(w7y$UTi$?I0YF(-rm zBMILIxR?a}Tl9$@Og5s_^rq5mSJqtu-)0OAqa+r9f|=na^Hu~DM*(B3rVr=_%dI0i zc67M?cuOfL*`s4(k_A!9-cC5cLBNTR(JVR2@Pr!eQFcTaw{QsYWxlFD^%`E(WOz~G z;6-hMmsVOF9R}s^^SuPB5oJqx+Z%5%3|n}n>N#D{Wj8K@rVXZHEiDm0ghZhBO7_6_ zK$V<>0OKFPO5(5a2qHCp!kBvkSvCHU@^^P8!;eh*L67rKzp^kk6j^iMM>05i*M_MU z%f+VJlD8)w+veucGq87>1IL8@7{~3)5P+T!0Jj^Z?cvUgQfUxzlC z0R^C0EHu&eXT3oQT!p9f-)@4^GeU`Y2_j=4q7!g&IWBsw_yeaHt_H7$R4hzHK%R7ck#)L#sr1Iy_LlppkH=GH%j(=Lstqr@Alr-uwS zw#CjE0W~#Ye{d3Z0RGS<;KuzC707+KPInLl0IaDi@mGeMznhWqpCjXqA0S4ivCL8D zX{qtB8?NLXfQRC>TA1d_^8NeZ{k!1(TfXm9jdh6FIF*l!@fd}cu)!N_5qtDN&~Rs? zaZo96-L8<;X>(T>&W6Br%LdNlV&^O9tokE-X}iyod3CWjt;Q8d#XpgO8hTRfWY>t8 zr|^Xo#@70$#?I4X=jj@6lYX5M2b`%P=LC?45751y=JrBD8yL%<=U=4pUKQXyv);cr zaQ3eEFQK#MQ>eg9Q@_}Gk*DfGK2XG~VcLaf8`t5-eWp-!mgrdw^J!}2D_^$eB zsC>~%M9olF!w2Eyi^eBjck_jO{_Prwssx_&*z7czLqI{zCQ@;F~T^Zjqid%FMa0OQjK_}|eO=;N~1LTshQ@AsGa zT&@p*027z*Y z8g>`rekzibf-N--f_S*AYzU1LZAA!d7y8YB;R0{ay+p6#u(zx1&$f<6$sik zKN;t7#u<`03~}cnjys>kdyEJHwYMs+JxgXh zZ5a+p@0+)5|6~-l%)O$1s4fvA^ho5u(&_3!Qu?0s@Mw+;| zVk2ItE9?`=4Q@wh#ytg)#V}h?`=xfAQtc}gx8ur*3}#(U1IRrc4;XV;l&X1~1Xm8H z;%NHb-$t}E7;NK)vic#8?c}#t3OE%gF&o>N>sKVraYdW4xdoh(gL7hRF2BFAn1-rfS`mzWP4=UMlEE3Cnm{RXihy7m7 zXIa5a`!AF-)+I8&H~{!Z2oT2^(PD|5GD00@Ih@2&57e;?-gvg-xuW8owOkj0q7T+D zXwaaEU}%0p0Y%3pj&B0bM1j0=iKD%cSmMwFJ*7t#5j`{BQ;6qb^4az`!exXQPE7@! zlI^2}cZ!5A%l6Nj0tua&J6mDY*n0`D@PMT$a6M35Xp3^}Yx;;VJ#Af|Z75}1Sd{sZ-h4b*L6b-O-F+rE$ z0r#bJW2b+~MFXvy#m$>rv)#)RB6vPPs2Q0ga82ZzHn8K;X!JuR$T-ME(~Sg0hB=jt zOc3z=S5$q3Fd-C^(==fJ*~y7$TEx1Rq_5vO3ydyPOsc*| z3k%$PTrqQ?XQNDrW&)O{jK%wRo*2<&>T0sYc+xzT*m=lFOJXLM+koI@QmRc@GO;cr z7M9~UtrAs63)UF1a%n873U0sQ9_(af4@S1lPHZdW3->Jg@`{mK@G;|M`!H|JM&5#i zxf|Pm^At#!@v;5er$E9~t?j>M3ZycT-Z}*mmH=)4ZA5an>AYUXSGZCk=l)Z=?2Ac~ zdf3`pgl(YQ0yWEo!t7asl?Kbe!~}j5xFcxvV_?qcBxL5*YF-Ygo|T@~HBAmKnQ=dY zxLbmKOo^r+465U5ZR|f;(>G~Je~Pi4?SG4t85z2H!oG$uj3}vF zuciAnc(}W#b!!7`n2d5HzUP@HguQieURm$uf8W|=2bl<0PlB+}M7X9ELGFX#;brVA z>rd)VY6>&;hUXk`MAJ+(5%?gEYMOx&N7xTJIXr7F(KMsN1;6Wf8C#<8 z?q)$G7FPqiBF-2(_Y_DNLfHO$ra;25!uH>rL~4E!{?#>%+RQ9`h(Kskm68mHnFZIINH^Ln-Mbx_V~5UY9Nx+np< z4=i<+cF+g%a`))e*&oC4XU+dE(o};_rRrWp?Z#xbc`H$NU|Y&8W$RiEB?Xa6v}QJg zf%7OjiNYrOKtlZr;-L_QR=mB;;u5?b6Y@sTN-Wj0kKjpM1SBfVjKtbC{izCS?pob3 zY1GsOx~8sft*KZkRf8fP15bg?vMcqacZc(;(5SK?cgeN%H18h;SHVJdDkxyWAST*m zAW`O0zllm@gQW83_VR=gUz4>4let}@$CgWa+*Y2V2PKgl6o?g+=#9utFaqM9^8^9C z_|d|Um<%Z!mMIwpF-gqUq{(=YbW8uPSML)^%OG@VqHx)jChanIx0G0YT+!f2x~1ym z-Cotxy&fhV`7I#FR_X|u!@CVKm@ph(ty#vV&Na{!t?bGDFdeFKCnKuCnLsjmNLx}D zgWQUQwz>cih$N0&N}ck80Fe8DCilgZLU{tnNof;nBOz^6MJy0b6oS&dni7U#3C2zZ zmdWUUz`u85ZiuoE#>Up}otPUsGX$?lbUMOEqKlBKN{rk^F(+h^3Lp>&?)U?{pV91o zEmhXn=v3F?8wO(X0P_)z300WLScn;3l3Qqei-@?~+q(`5UKh0`I=s*rm!Q+kAi2i6 zztDJEf(|pw2>Lm&T<4nLt3z-w|D*4PD>VKf6zqn`RD#E+5X0sX9IW!QX1P{_#7Q|*Fsoa}-hBP5}Oq!muu=BU-AgdT=eMkUlfNeN;4Js}bskxFfOLaril z3+EL@#|e2}DH@SaNJUTqX)9EdPbeOPJUbJfyl$=B$!Md0Ot8)Z;%#I+b+ee_(Qk@J zNI?ogb63B>tZK40u%`(j!70lEcg>-dl4`lWs6{As%cx2CojO%B1vWrA@rDLC10?c| zC3xz^n#KWe2D@yL(;e(vyw%!(BaC=@syhLy%Ivq?)inYh7o^9+q>FX3gE0Sg3yf6FWjf(R5|n!b(AbDV z&{Me5M_RlGY4UWSzmZhJjLk9Vs?#V3@(5f)1)y zm~zI&wS7|R`%(}~&UpmOZO|+o#eqi?(|l(GIjLfQ0SGWfabWK3CS=)|qBObQ%(xx1 z8*fEXxi07P7iTs;@WfVdc z*XWy6!)~1OKRsp^nh<8FWBIaJzg25j^_QjNao&il7p?JXh$jpGygU*YPDCOn3qn{v zl`C&2seCjPu~|UU+ny>6T>{V0+6k9(aX4);u|&6I)KQ__87UghCTkrj_gdT2IwVq#LDPXn5mku2z~YH?*2{RfiiJ6ZozfT;R(92T%{i~Ar3JJwSm0E7KYGmI5Z5H;6Z-z+2@KAHCt+&J3dS!55i$yEw!)iF9^NpQqs6T%QL|FAs@eRBat*BZXojB4S?mn2hVry ziA@|(S~+Z$x(zl30^bX;2TRl5A{+hO_c1mV(o9tdgRwov_U=Le5<~*;hil+geHiS} zcn?+MacJX$9Noel+AbGPelQrshT9*99_U=N7wknfoxyVOdf9{}|- zS_-yA9iq2HIoZb+qAAN}_p0~r2zU3uu;@YpDU>sUrRXk&k7_yD(GLN``!F6zA=)!_ zPk2W;_xoIDDSkH^`+dF(p7ke&!eayO-Av#kc;HeC7Z<)XdS?QsaV(y=DD%}QHe)}~ zjEen3_Np^*LwnEs(X1KQK=-a|0vP-nOwql>zGch$HiA`xtYCxj+;bE?5|1?+`?ltLu0L)<=W*?Y!JZ6}Ige?#WvU z$AtYe?#a6!^!vsb;x0k$xOF@@T)ctZ3Px+IZbnpW>1rT#IaMF*+%K~FIOaMhENNz{ z*qW!V`XH_pkSuj=uT+XUM{v#vax}xKwC1P_Il{D(jwl(zpkv@(#fqa@>rAU|FRSss zN;ORWonX+Zrqc)h_%Nn+tJfckHspO0xuwlA!+n4*-q)NX%1Ycpq^s^zgxet8jmxxH z;*IA>xO+0fyj!?!1@`CBh{JrsIk+;y-(YB%2uC-?`17NUAv?6;n;_T zg?-saZ}WYU<$fCJqr5Xd0k{K4!jS^^$sdIG(UkvaR$^UECIIk-A)c(jw8T*YNN+^b z&^Z~s8BLT`#DmbzC>S;B7}DN`-2hqHfr`6*@f9Wd2Emk%WqaJWtCP}@G9WgZ7}9iS zP}vXN(}RV9VEKk=1vt@ z1h@=(B@8uax~#ofbR)jr0FUUw4rsMEp#^Lnv6@U_1N1h&2{+^uy^5I~zD1Yot8;0r zW^3g-whx6N56U-VrH`~`A_lW0Jn35*^!_|-DiyB6%`@d-Xh%ktJQVC}Cj;2dOhh9% z*dop3EO#ci3!s%%mK9Y4tiJ4@C~q!y&jX#&2T|6{FMGhr9%BzO=MRp~;-wCbq(3K0 zzo0N1s*aO-z()%ZS{v#){Y0NG?br5D-r^ZznqE=C=pG4MOjubF6mlVHq$`zpRoo{# z8(a*@Mp0d)81oe5ZnKxC$^i0oW>TI)aq7w&!a};iZRhQXa&96@M=_AVuHxiTDiqrb z5^(% z4pM`$%Bp}FfE~sKr{uJYgm`o1>W=*(qf*O(G@|K5l^9no&tVpn|Nozi)Iu3?KLfco z1qF&~ZX~xEwHJ8$XW9*#9rBcuuOA#A$bB`Ed%EPHILA7eMu6Ne*ooZh%bBfa1<^zk zwPUltXd)E#r3YRW~gvQJ+Ll2;R&<)wWVPMmT`>>nz^Gz%rFRmwz zWfW4NaLA6%XNG&gTl6{P>8^x4#kzV&jmu~HOKvq12s9e`gD6SCg^G>#;eWS&UJt%s zA@vjIHzSZnT`cm{|oWa!a zss7Np-XEaTy$dzNVOGXRzaxeb8o%DTj$Tgy^EJRl?+aAsv(pOuvUq&jWPJU_3{ih( zS)Pi)pPbt^fr5|u_28QtW)PR-peN%QnKKE15XKOh6Ixy|K^uY8o!l44xz|aTP@!^o z$Xg^kCb}oG0D=D!DuqXa6Eh%92{brh@tDc8MuxlX9qw-l;DOqg2*Odnlaf?gVFHIb-r%tX^jYlzf0 z1=4;*qL$hjr56!t(G*Dg6KVGyBeASg`!**meX7+++bHI(UtufwDjuy%DBpu$tEgW65HIAvtx_x|r}tK$yC3Wkfd}b?01a5=RAHJKKrnegjWEcE$I#L3c!Cee2@@ zH@}I;CglDed>+Qqpl>nK)^9WPJ9vyh`rnMVOwl&Q{D7i+0uViaIr?2VJ+vWAwKl#D z9iA7>Zp%};D*rza$obdr6N%%Kocagaocydt+bnHw{Q#i}duv({@WXV#ydYpa9Z(Mf zev}Sa6$Ct)4tQ}8@Z)sAi9x_m(g9u&@KieB@*v=+>42MpfS;uU-W~+}JRNX%5b%q1 zz-NPir_%x72m*eY4)}2p@T+vdvq8YG(*bR>b$$FM9WXNp_-#61k09W8>3|mn0l!ZL zc+UW^6N`Bhly8g0eCA}p&65Gooebzt2HZ9o@UqE(W0L{Tm<;&J$$(oX1D-P(@T|#z zXHN$7CIg-q0JocZ<3s?H#_LIdV7uFr1lq|hXd5R(JEaBf)XC6JYe73bKtsc?ESsY? zWpD|*=puZC12#hXW|h&sf}zi|kQNMm(BaF_2htG@eNY3@&<6z{!67UW93>jTQIHWF zvJ=5kKM@?&62T!#5gg?m!BNZ+{Cor6FyNyG`~m}hp#i_haKFlMUu?K9G2E9L?xx}1 z8oIr`In?7+BLqHRv3UbZ4hO#Fd80gUlII)bc{85q0>TMLSUebRPuOT1|5W@(gZOAh z|5W_12l3J3{;Bwn1@SMB^nn&+YO=?`Gy7-KFIJia7pT1n;f#e49M) zkmuXwc_*Gm`JrEx_^*!RzfuRPy}rx71rvc$hKj{kna`+z+EU7jD5 z=ZEC^VLXla=)V!a$=1^R16q#%ACV1IRA?joz!m54i4fm#bqoSJbGeaHU?y74>k+=i z{ZCNX9E`?C%K1%J^L2oav}2uF9365fl~b_MN3_0*_6?Jc=gpOT+%>R9UW{PXBUc{dI*hNnDgP@pMbP@5PPQO3vyW3-3f_JJEKv*Pr4``gT8^X9cf9~`!gBmB}ug9D$sNM;l`T%Sp+qIoqiplUkCA3^{J%sqGra6x)?7i zV7#bu@uHr^i<%WL=*;lOmaAgt)v@!M*m-T>y!0UdHGvbm6r?zjs}%=u{~c*w7e-m< zv;M6Ga>bA652+k(*$WdDO`@TqPJsK>7x~mCj0UHu7wx=-(5nB&I8fT8jx7ySJEe7M zr!-NfzrQc-R6E&N1M;`WPHCVznzT~wl%}ek(q1(MX|&qOmKzXzN9>f=s-sDh)lO-* z+9{1!Q;-&{oovPdvAbfYG+iA{+OKv>Bi2r7$(n*RXYFL04v5_?PRP$%|097DlX4%8 zq4&hlkHye?W9Y|Y=zTHt6EXDuz=>(SPsY#(V(6!0=%-`ogE90oG4!*cbG`o%omKyH z`t=>YX8BL>70eb&`&f#+$=nQXK9r4I`tFioSDA%reGK!qDopQun-h(DffR2t z+lOxgRt10;g@C;Sz{(J?IsmK<0m}oxz9C>u09YRaRs?{-5U^hWI4}ep96AmQ9Z;v5 zqvJxyi$ljzq2m>yV}uT5L%7CUc*9#bz+2LpDjeghPV^X}a|-IWDAN%O73B?j}vn@DN-v zcf~u^&CaY59}7UHxH&a}-k5@RCnitdj?fYRrX-HoCYq=w;!F~$YxcxUY-gI|%=VsR zcdKS!&$T*!DLXDRY6zM4x+6J>O}!An6D2`y5Wvk0HmL(N-0jCU8{E$HSER{L_KvOP zBJJ2({T<PP!$F6*ZVacSpBF+xCRnp%YIzi-d{}S3 zGrzw}@0h^tWqp-wUk7&S`icFHObrLidz_yB(-ZA15`+AP#D>sF4DuTiTS6l-$Ztq& z3XSCe0l7pHOuH~rDWq@+Q5nA;k2HO>3n0 zZ>eyxZjJCarNXzT{BKVAZ--y^uV$)gE|~AI_f6*i(G)!~n+<-%b~gNCKpTFsq7A>8 z(uQB`X~R#Wx|QEF7p#?t->$}k=rxV(ZDp|g4@iFFDTH)&iQCu^YT$AeCQ%_`${P&n zA$Z*G^k6*pi)C-%>j8QkE;haqPD8&ngJ~`(WKjka!uD8-Ul}bJ^kv9k_+{K+_&=D6 zFCz#eT!s;b|L#foO>@B}5b-;s(KaVpAq>_PvO7Y$W~3`5(M0~6tLv5BoZb2p`|F$6 zkvr^gUEnO}pAmKl)B7sZ`YN534u$W_in{Ep%;>9B`YK%&2lkmGu;m9i+K^nn#;It? z-p&l|*u!BDj{8-5olfnjIn%VG)4>)|{GyYp3!R1iiQ`SpZ-c3t_c#skMD{~w7deYo z^;w`{N-HB_Td+F;;r1|uPwo=Xp`AJugZ9Znr|Hn^Cl8&jLvKolibCsi`c}P%xk_{7 zBvdA63p~sow5IMP6sh1Lx0B!{Bwp|^&X@!*p>zcg;|Y}K;LoRK6Q!3OGIO%4d~>>_ zHR3L0gp(_+5qBYHD9lMB>a424*|}tu7B-K>$yTMp$kEJTV$!2?p@Qz8YpbfG3Wju>b;i;;b~e4&d|543d8h zm(fmTv9nmlJC)s`2QjR19Xjx}y+vlM9d41T)SY^NS*zuWt8l8oj3&14?p%T&Tf3}f z@ZjveK{g9B)kHK=k0u?FYB#ZRFGjFdf@tThDkrfWhn3*$Fg}|56!AsKcNia2ty9Ej zrS3TW*G(B;WPXR~-!Nr-3dxSsfBls4r3vgX{hOzZPodj!`fp(T|BJT6IeLSH{swpJ z1~~3ndkTA9PfpJ*p=YA?gX5OBq(>v?Vg!m=9J5VuPhR6gP-pUqFw7`nDs6+=jHDU+ z`+%qsO#?Y)+5+5nGT zMa6g}Koypa+!;=>Xim9al^8+@krwUO2JECLAnc>vk$-9w) zf5wjGQu@dD5b2duAbo&HXH9|h??gI#3Z#z^Y0DHyA0^T`Qy|?(ByS3&PY~(6DUjYr zq{b9TcM)lH3ZxGa>B1?HK1`&Gra<}_kuIJB>0TmTG6m8niPW3|=>Z~b1yZ6TL1&De z-m8|RI*JxS`vPO5N7`xM_eCNBbEimOA`&oniu5p%fVoqoFB1uvJ4JegNWk1F(pQKC z%$*{Al}Nx$6rfp9v_55cyJcZ?Q8YF-#nZeiCqX#KL||W(rjWQvnte$ML2i=P#~R|K zOrrZEPGW6Tl)`Pujfr&?uyC;CMR*xt!`WOnDV^I>bgtHP*6x_j0h7|f8bE@dYc!qp zJEn8cq;%eyqI0dLbKnl?Bp2eOY`z8>+3(D3U^6uJr44l=9A~LWwAE6x5iXzp!wGg2! zY(s&&JW>i&;O)zINXm^P-v3+muO1QVrnLNl;{7zB}H~$3o&G0|K zKcIJ=BzHYMhX5I0n%GZ(8n2g4$LTN}h0Nkwt|iu1 z+zYKiIOCX|RjSvlt(o*Gm!G|UILI0PH;@vpc=vdGlYA4>cc8c#!eho;QaS@ zkrsBuac?EVaaIA6ksJIxz}ZfmRLE_@(`w!cFApioPJ^Y`0VL~CxR2v*1i(0f>~!k2 zCw=KoSv7u$@|u>>dl-I3x0T+*(0CSLd}Hw+kc>vhT)5lPC>dIwB866cdC*dS#o`if z<0B5_-Od}8+sjJ9;Q*cuIA8Y+`h3HKzQl)M;HZO?Je=6}{T!)k=U-#z-(u(A>8$$y z#aGS88n2*(2o`u50E3u2uc2t7o!8tg{cGTJ-|=txCg3*#;CC7j-P4d$H)j>U>zdr> zDwCVCJA0`)Rwbv(Cs9LPXdnrz`jCm8=+`pfkcTI1cQ+JLnno)s3MoJp%0_yvxF|*2 zT>!EM7FWQr{q*-qUR&d$*mo2c0&nKiBrdr@HQ7EJnh|0+IdsW-s5#qb!!tq*r$&07 zNN77dMMCA};@mW8OY;l##BUF3@h?D%P+@cU@{7)$p7XEQId<}gwB-ndb)#EbG0gIMI% z%w}^U`pjwq z%It0NnLD$**zkGbq19ZBkfnL2fIDk>)i19;oEO|0UEyb1K1U9AXY{J?;-xI7;4_j$ zk?)w?$yPl2D_k_#*Vq#Z(BFqw5Sqy`zyAJdD6{LYO+%Sme?6f!e_#C?)UVYVmCX1r z61UYPOS~AjHYD6R?PvQdHTD|38f(Cg3qyd5E!gzWzTy)wAvUG;(?*n1dLKR(PHzNm zLe7Zy0=K^>cJIC&@%2qz9k^E6EF$jjmoaaJJ&@icu2j1kd}oSb+># z;JnQwCno?BJ94{RP`k@AS7@_Vu$bQSPa8G1k+od|ZChe78UewM`|RdPQkD((xnMN01R2*G?%TWCC)sc>Wm2*A%T!8| z4I{-SPl4jf4}8F4)zz$9GmM5nOHOGF2vD|R^RqcfMYiJe12}<6)mO<^Y*ojt-bSvY zm`>-0gy1xzgcySBCCykYStmb|IGLb#%iF1K^`v6Plx;pH;7pK8Uh+hiOpM8+m>9Ib zXjaDKIWaINyC`avi6z$O?k0qGX!4r(hXE1QtUoJQkb> z`5?R~5hOgXtLPpm^dz^mD{uo<9RkEtuF1_u5}4P4umcb%^5#?A{_nYRsvw8BcC^I? z`o>b6b9`mG-(X|@-@AVtUde5U{|yk@7#&Na||#_cGkz-QWh7wzXGOxCuVnYLvr# zCShHcwatn^_%_VgX7S~dJg&C=0#ZV* zF_DWxV_;XtTZx2=C+n%J@x6_(nzt{xRO97Be?IR%!ni*G&%aFa44(A&1qRfbGgTR^ zp-waED!k7Ynezrg246r}g;#kSCVjIu>S{H7{WA*}!j78LUn;zd?~WSki$hdiMYp@^ zYAaqx@I;kQ4<8RKN!3{OrJ16)AG|0pC&S4!BH;VH0@F$4C93x#Vu3~QlvM+GZx)`i zQnYNo`vW1odiLzh*&DJqoOSkz-+BiLUd{(6T?d)l(^A#bg>vm88*fu z9$iluv$tdp@?VEcW6mG#m61#Ut4+ONFA3X@Kb_@bI zxb_Z)N5R9fu6Ib_;b_@AH1KfD9C=om9uA|U5RQ@~&wi$dBkYJY$L`)?0cj4|BhNb1 z!(n?Aa)9YMF!D&2?Ywt5sGa3@pq6-3Z@uarLGt5O$Wy_qi#*|q=6v8>qT1%7i-MMY zYDSyLk%gN3Q;&8>)et#Yr>3MEp{%TQzKwbMbC#m$k~L@ zi6Nypd)NE0k;REwOY~uwBE1ES`sk5DD6~vA_;MN22lV;pFMHlcvj(n6ioylLn0(J# zIUiH*S*tVaV_qEZoDbEAcU~vmS@0#@PMfB77E`Y1ad|-@=hpenK}-!{jy3B;VKEf? zgOfx=@?MOR)M-*dX>oRs1Q4_0;kEV*$PScn!6BI&uD1s+cRD7mFudT{(rPm43Emqy zL*;mqj%|-aNLEihB*pA^W~KGH7yOIi;?UIGqq`kUd1D=_hdi3OPJ z){QKi9>Q2TG8HHr2h3PHp@2^oU7%cN1uW5UDbC=w`R#z51{&L!C?hn_)EbGz&Mjc z0S$aOiNpd8d>A@VB(zU%`E5*qywzlLm9LTLj|B#eO`4VDveI5xPJ5`oF?Lp%T;;k$ z)CeHKY#sx6GkF_g9=Jrz%NePTWhitCdc7?(y)7sY%Cz*}{QSfc*f8x4dtP{BrM~i` za9yrVwdtFYV}=`708M`{2Dec`NiSS3zi+Nx38`SYXXI-X(vyMz=+4nY!9Umm{~4_y zvRXmp0{xH;^g~YT1Ly_HVP{@T6zv&aX-xgWYSnjV?gN@K?yxM~-ug-+<~H6ebKJ;_ zXV4Ty#$$!1%QYwV#L?udaSL)bKdN-21~HLk)NV`7GmW_BtYX9^sbM6Iq!9` zr{{F zc{0Ln%*F+jjwL1Zp|nSu!1Y+i8EteZehsgFXo&hoF!6Xt5uclv4xpgyYVme_`HqVQ zqV{2~=8L!IYeQv~&(wwiSg1HJv+ZCQPJIKl#oG>o zW5WI|?dz|jKKjNO;zA1Q@>wtQvrUkXjH-b494R|ZTW4K2qQti-qwkvK6&eZGnQ<3X ze?E(kL-FH0XaE2;HiM;%_c91g{=!qb#@cg44L5g<_YlFAdiNTToth`j~)m1+C_zR488++kQ*uk zeM^|icwa>-`~)M%a7XK}uod+fE#H-wF`DI~^RPKkqXd^#!F##1lX`E#J07m;*+HkD zQR7EuWibV=)oOgL%GgyKIe8RmRI&xTiZ8TDt_t2O5V0Ik8n5CbVIkFVrSLK9!IwY( z{PX#+H`96EOI`a3)xkWouG*26nr;VFa#D$l*-_Ak2A{%Vo&|?CaHJrgnMX7t zgnELsb-&Z3#*AsxC$ueWK?rqZ69YAUs<3~q!k6w)S}0_UzOvpB3ksVGh@qA&fzw5= zAd@8IgZ-3FXDfzxS$7!d&>!H)Vy`SDMgUtJq?yLwfH2+3#rP)({t~N6irk*^TIhYI zj~78!b@W7__#_^V)Ub;IBSF23K^)+V{Jk1)`B@ptk?mvf2YX4LF3Q684_nO#8i9#eD%expJAEQX=yECF20)DciXmbEpBU7Ddn1~u*UQ&(MQz6(My3dmB zeFIN@(aIHYK}BO|ZJSs0jo+_n?3A^j55VAlm0C{OXSc@+@Gc@bz4vzoc-L~IgIH`@ zE_zkt{b0&9SqS1wK`V^Cv;6Lix3cBoyNP-$7gRpSFe>(zy~^CmnooBc#+ zo9(+cmgyJ9OXGhKFQfm1c)8&JATN1!A?6uh#rBxfc0Dc(g%}Lj?7?B$q5IC;?Ssbv zE7PCP1O^|@j;#SCu=#MeD57Y?Z(2`emL3)weuu&_VLuxiehf(&eg=({^`}EJ)l6st z>MxCK4Zo0EKU?*b0`o1P654Q_NMEY)uu)5cf>?TE9L~<-6Ej7M3E{bZL1tcRAjjP~yIz;J6 zuh)130D6{!UtzT|F(G%1u4iY3-{7+&~qw07{qas3LhlKS6Xup26tS$aA4x@{g9J`%?vWDZx`ZW zoQ?dUgEP~Ea4FqRLPMrXmqMd}|7elb|F36~VFjBF4u~$?1htOH| z59O;{#K%9(aHE|>@FNU2CYQN&_(%hUd6Ebfvm2y(G+(`AeMkor4RAi4ffHsd5Pg`c z#2L~r3 zWbHkiW78;vAQSdx2+Zh>l!TjD>l|lfL?{2h*nVc}dfl`eB>%rzqZn`HRGX#L zR_GygS1fLPQ5+j}0ODECRh)_ilnoivdwrUb&w>#hRg93dsOcc9`F}_{^aL#Q z9kmpAqn1V%Fu_<2e3tij7(ZAE&!JZzSA#R453DBjANp&~m#+G9oUQ+ly>Ee&tEkey zx!rxc`*zPHw|mk(Gnr&22@@{qnE*)$lki9g4^eyovO1H9LJ5~49)14U-)OB@!N#CkdRi|F3 zPMtb+s!F#1_fZx5p;V0QMA($MnGD%&#j4CYj>y^`*ZgKf{R$UqFhy5a^)+Bk)M-^; z3y0Yd3O|)Fh&e0-B_S+h=P!V$a|LMq9EAl5P7|i4yw%5;$qAB)`i%?~-2jy?BU+f| zW)X>@O~c4K0(-cI@rdDk-$+q5FpQthaQ-ku+_d%h6+$74kydmAf>v$~n-9&d**hC= zr|dmiOlh-(-%`1Bb35fTx~p2XQu;Y>Bx)aV-7-l@6OWQ*QpN^Ub&J%0>#6Eadr?*9 zYDcl_z^3eT%<`2m#lJFVxiILq^J)2X^vd)&Sjv{RZ67RW%lPfi(A|@Pn^ahGKacph zMx5{2v#nPluXCAK_oc9n;$`TiE3d=V=jAJ}vlCFvLDVb;K>#8M5wJ519_tIZ#KSU= z|GCP1+vGBzw<~3aa1kx@H7N6&b(wp`7Dy;JSGyx@=N`0k_mw%cM;;aLQ0-kzss@Lp zyEJ^!%BpXAp6&rBEnNV4nSFrq$4+o8`k;lNZw@4CeEQHQ_{?>Z>>&)%lDmmx77AF$ z20^fGPeVD+kChX>5-nx#q`t$(Yd1Qp$L!Jl2u|7`L<120$Ozr+Xhc(e3gO!7s2EwD zFWU!F560ovQ~wvW5Ae;qE z7l4Eu*#CKdG@++S*XW6~c6DU+CKT|p->GlEP~nFlOg$t=BziI!k(P`|3%!?1tBzy3 zNV&)z{+#84BtHFeQQwTcCG?XEePg6tebQcYqwUozpgCx-pb3Lk4fvc4t1?@)nDmL8 zJWjW3)^~vkYFLlo*5s&~4N6<@AGS5t0g*ZdqsEd^W67wIcJFC!_elMqEBs$uKP+K( zrG81{ShNv#zOgSTT+UX@MeY$V#F8j8dZjSIoR#TbNA|~Wd6XOwTg0RJOtO1DjfjT- z&HXN@(yBr+i7hg1{7~G-5QT%e_;#QKRH_(|J48=4d{=hfiB59EBgHIAmxbJH8PJ&+m&lkAxfzGK!)40r(Jnh3@*{EQ74!~4$Q)+l#P&B`ofPqma5K8L@kyt%YAaWMs@QYozzRB)LMF6%QH zC$;rd7+tX*`|A2+#ws`kw)Ir^dSEkDjJz*utEia?8vM%qZB|~Xaj6efRlq~!TTiXb z$Z&pzxfk)PL>okbWma5O&33ht17NFuIq}RhW=1ODQJJm~qww^UU_zwwiRUQARx28w zQuNm}3m0-t!vyYqsMw@6jn;6KV``8#U8wYBSd;L;*3e9d&eW8d)&G)JiEmfo8CTDq z!4(SjlT)-0HZCP?CHf}#rVm9SF`*5swa_tWkeM@N z@l*R6-Dc~BklCQR3Wq&KYvnH}QP4y55|Ji5zA!(GwqQ0QL+fSjFMvF{tR2|{g7syn zl3o+x>VRPFae}{MopFNDb%2kfTjM(g>7)3rBR*MUB?g`Wo0u?@0}tc)8k$p6 zRyB?K5DChc=`zUsIxA!7`5(*>=xukMaA=lrjcpIpd zDn2k~zHvk+r4txz5g(zj)1#A680ir+p|I1UlTa9GQS%46)od`w?St4)XLgL&k0)*D zX2gDsJZ3F$7~Ie;aOl^mP82s*L3ATd&P@09g|y`nobC_fru)OVezznzd{;gU4rXYL zVRGK_?0g(R!~$zha^Ct;Q5C}4v@h_e)akx?$~r@n2oEYAYA7MTI4j-f%qq=OOh-p> z9A{|0BGxkoq5YrLa^!;Is5jajYaZ|$|jt?Ep)hVl*yn$jVUUx;&$P!4dKr@&7WuO35$EB8aQp8 zaLf~mMW;A?S2SDUNF^3i@iNr_>Ar2nJPa*W&&yzc7Mb8NvsK5zz9p8vSaM239g(an zY4jjfzs3j-x1$xQ<)G1mT|FB=YDjJYx54~nGvpAr3re5aQU=+0(~y(w=b~H^E&R8x`Gl=2~tl}m-R(rB8V(MOtjq|Zzl+@Bp) z)347}1!!yF22)tX3G&3kX`of|86FHe^ntCQ)1=R;iLs8k}>urti7`_w?K9f#=k z53snjF<>lA%lw#$q)SVd;*ya^WKd|%RNULyd+0TO#``$?_!(a`RmS@|`$jVUG&4RQ z!>)bCmGxMu{E1?5skpRqu#xW1I!iGdY%4BXh$8WC4FJchM~mGHfULOOa+W&FoaHAt z%h$Va0c5-3EQOT5)LF{Y@cTLY4c~z}K|Wss{n<`rnct#(|Bz+$CflyaGUb2BG8jhx zkY)ZM%lw~_Wlq}b?@5+95A)0J$}$0t-H>Gh9J?XQ1UUX~Wtr3eTC$AF_%9*L+=`_q zblBW?rBoQ|W!`~t`+rrI>Gz(EQl`8ZQs+iWosf2^eVT?+rswZgkhx<9SfEzQm~*<5 zIQIK$E)7Iu*Q|S?x+c)Q%!KY`W~d>NManE^mcJ+#-OJ3Dt4a{BuX~}g=C7uE!8@W; z>t3K8+AZD7v@+>(x?1R7ppn@X-3wGUG2IJwG1vozLAvN(phRq`d+BkyweF?I*S&PN z*S)BqsdX>FTIiXk?uGBE$8;}!(7p7v(7o{4@u=>lZz?jQvSycbFTrjT6r+L4Na$(;?Ko^u}~ARMYIb?&bWLyh1%rpnIVrW?H(J0EfscRM`YLsEE-x zcCLE~aEQD@l}&(yiWrS!=em~whsY~b*#tPKh|xHvqkDm><~i5BK%X-d=wA5Nr7q8u zx)RzaZp|cFnQs3h&-R?Wfqt3GJmb0(#cR!46rt1u6U#MZA1ELycxwCxu zBd8wKFvGOdw2Zd0%Qno||Bzj1>AWklOYc8q7YZ!@kXCBX4_E4#pY?yn=esEq&8vP%Mb7ucXp*1L37LfPdK zx=r`3&^@Rl_Ti}vt%~@Yu!r%jh^DPQnKMZOQTLwDGzJV<)`)crZXwGW$|lA({wh{3 zV%goF6NA)4dzvbg zUZ*#%LYV^<%A6J|6xz8&RVZ_&A~Wh-c1eY@6EOs4K)wov8XBrldRm#U^u$ys)VA!p z3WYbChFd;V#{_Bz$|QQVy>s)G0EdVn?Mz^Hu0jcLh#1n&1ZL+dlmLf_A?-|HrlUfE z#$_e%?}dHRBRe9ov!(INbaf_FwuIU4RA-{Il{2T7@iSP8!Y+HVJaEHgc>wzKzb|>9Z?Zg~ zWPtx$^FB_3O@H2}O?fcycVOP{XqxxAw-TNAJEmgD6cnD*c|WV>eVY%tgl4O`mh-+- zaMZk?l~3z=ACk#*=Y1PT=_bwl)5-)o4mIyb)6-&hSLS_)3$b~hazJF>hdpJ>dB5zG z^t@m8=lxRqd0z!hJ?{f+p*+obzsKo`&-=ZY_j_B+``lxR&ilPnkr@Sr=X&197L-5l zQ>@^;ukW?s5sh%`q&qh6Q$*PHd7s@F&ij-hf_a}pz_jN50Ef)`?Nk^$pZ5bCGVizB ze%blFAK;LAzuorBbmo0*yF3^3J~wQFc|T$~JLPT(WrT17-s!xL17}v#yichinD-+F zvQzGr#0_M3I`8Xj^}PQR=m9S0yf4>~W8)-M>9=t3=`z}|%F17E1C?uqZG9n+T)~6u zd>m`|YucEX=Th|f_<^BNfp7!d*3MN2I3P9e_+i}ndFs6N74<2PKAMAHrr3N5t6ASB znYRfvAGhIYmVoeiE08AqT|I7-b8;a*PI4Mw`{On{x5}8C9#yAdc#g&FpE*gC=%1yU zAS+E!9;>iV`p^mz6x;9H4NvC65kxTYex!74rlz0S& zQTqE&I5E57(GDbqiU9S=!m7Cg+iCEjg4j^wD*B&I+ylk>j!m*JR{`B)zO;@)FlJc; zP`Z#XFlVm(T04yeYR-gxW3h7)JO}b(oRegLYU(m0mT`G1K`};+!yf(|BUT{gspc5Y zUU+USf?du=NFT`Ih>-jgGH)V~#rO04NB%+_K}m8WrLw-jsU6y_+g zMj3{*7HhD^j*2zB%8R+7C7RCT0I;ptWjI}vB%5jFgPC{LF+7kHO<&}M*%=ZtZM9!y7N8*8b!pcD*Hw@2qhjhrmFy<`W;1I|>JaDdMo=-k&nWuYQM8*jxGRjfS5)P%FfH)$DcXw67p^y~d zqqwuX5)QfBx|ozRYqFFxQ^gWy(UA>hnrtZ2WJ8H28wxbpP@c(#;!M_;X0pC8lMQ8= zY($jFDoG}*1evUoWAc?@Fy_3vJfhfvIDCrkhZv~3AF|Ex zeP%Y>QeYuCBCgQeQdoiDwDLi7@7@MVi zWmM(THPu8xAqZ>X5Vgz`Y+I31VmY*)B|2KmD6^D|;uH!|)k~qJjM81`Rx--0ov2=5 z^BI*%)lqN?m+NvM{XjMNU@bf(jQ7B+;gDnr9YHN zh+7FH{js7`q}nxsBnx?jm$yvJ$ZBz<-8MygaU_OAgb@lz0Z)$T8`=vaF&rX_P!bAo zL@Yepiy|=`B8X593UEX=9;PFRK>E?Dmuy=(1pGHHhJf`pNgDPNfi?EmSXJKl%{l5Ul7z-Akr2V0GeN&bsMfx4Mn7I|h?sZj4&%$^}*$0zd@cI>La%jh4Ku=i)ML- zN|k`dXg;u;8XImj1o)_xm{OhzE=ZAsI4ONy$_BP<|Jgj6Lw!bol1Dl;%3ZxLr9Vg9 zO+u17Y^?50$*C(-@+r|3buhNLig@0R3Ec~^=T=Ph7d0Lw2S8d31u5M77b=I+p(y2~ zLs`m6hr*PT4y7rFH*d)8Do)y$r=0XW-lOHfB$_vC;j2_aX{{2%9Vxu+5pale^6iuh z1p%0eg`hxYIVot33b@1xWL?nWRuM5Ar$BkDs1QJXBR&zh3LtX;SWyVv*uWUR+r;hP zxK5&#l1Ks~QkRO^Qtsm3$2X&LSF?pJT7lU#Nnh0EV5Kers@Z!PvA3i^v1ui;86k?I zX>CSur5<8eEA`;QmUt$uC&gmAwGdWPLKuuKqKcyKmO|LP!aOB}m3AV8K~)nK!jP|4 z6g6Mmf(F`FEE^6evPlk`Rz6VWC`C~;y<7+qRTQNog4oU!McCGeDT*MrL={B~+X!NN zI13dq;qsO}d{Jy+dr?dUO)ZK6Z($FmC`!i_McCm29W+T}lr9wc`Qq4~81>GcI&O^o zC`CjCvOT8~$S7%O@{3Aj!d8@?QVQf}(w6*`qDYlriDiSKSVp;oilV_#ERzhBTsG(< z7ex`pmR*y}z__*zyh|yH{JUJ*Ni*#wvnDJen^CJ1Oa_qyEbV2pCM+VIQIZL;P*l-V zN_*+735&>Qlu`mL5hza8g(8Qm8SrF#B%3a95w63_X-MMFC*Wl8vc+e1k>?MYBs)Xn8e$7Y=1VEypl)!N#jMpk5^C} zqy9EDG|U!)g>9~OGg>7tcWY+kZp{uWjYqOCrZj^ds=gf#-kZiQ!3L65NtOH?Qi)r= zKXx?D7$b4$ zlQ^a^7wv(2jN|Q%3+ni2K(3PP)=h`{nMMh5b-BX6;X`?h9KrXV7Wnw&0Pv{?3xQ{+ z_!xO|9bO7(SWo(N91u9)f%EGi7vM`o$tPia3g06bp1{3@Ag2z#h{_s_>_A9BXc*P; zvHQR0r3FP_v;2C>@~Jul?{i2s*_L}FO!`K`5>=CrcZBgUU{UcbE<+_F5MhLl#fwaL zhpaCbFUGBiH!9~6-c9wsi2MD@{Wlu<5^_{LwTKGBvWiZr;H3&I?ybuGtST5jqVB)T zvmG>MHgH8=^xc~KKw)^dhETj)v$WN_HO=Y1hEkq2jda&aez<%un%|dHeuN^wWv%jS zPAB=bO9xH$1;Dj<0mivFLSBc9gE{7KgDVfQ{1@N_jbhLrJ;6L^-BCMl6Pe`s_dR!ehHPm z$~O6Iv&GzM89ze4xV9Ga2p^sBOPQ#?6D6~)fg5JxTEuUY+RO%PjqwNe@;qGlE4MXX zg;?^6PCiqP}I2TT2{uC4wU2};-GaZ4`KxGwY>n8|oeFjJMPxWdmio;WjjMT5*s znrm|7SoI7a7UsN_qXUzcDim%Qg}q-2a2s%J;mUXGsc7I&`V9==)rhvpzd{&%c80u3 zkv7hJlOgxx@&$$ypHyzj8ec?v+#vK|mvUBNhWjt9yitNp3<}Bquf7{7v12}`kaVx1 zGi4<3w#ET;%6-V?4g*2&fq(e1CuSCs6QvE1W2zM*!ZB@yWy^8?lWH%OC)GMpiTZY= z860Ow^;$TCa%VYRW7W-fBv^z*^*RLm<+&d2V&)0-OWJZDkX#&jk8FmE+>H5#`+Ni4 z$iFdT?~7DjO(SL8@^K?aDCr=FMf!VL?}9Ng`zWMKRBr&@*WV9kE-k`izF^8awVFm* zaSLGD{RA4?y$wGo3I{5v1O1H2hn{MJ(b9mi_f~$CId$PB$F%B5G*zPdUKTwCVXBxh z9)CL`;C^U*yEC5Q_8j$)T``^3os88^ij3YyF`cyMAsaX>Z!;WWp`w*n8KpNGnMEKS1KsB))xFAdFju`C1;)(FX*)t1cBNb} zgfj3vw$!y$#e@VR0$8ZxNl=9mrwaX?j2Z@2%vpSwi20l7x;>Wp{Wrrn>~Ph8U}tDN z#m>Y502)RC!~p=ZRRD1S0K^I)4glWo1H=KqO+G*z0N~C?l|&o>e9#Ao1Aq_t0C50t zvkwr*BtCl!f`j=!B8!2hK|vI3D2p)`#w%rl|3m}x4@(B9Fq4OhjDyGk}0nNwx8CU|DU#+si*a-&v@sr;{r~hP`S?|Y)@GyZy7xsy?P?gS;(A-vljN!)DkxB6)^>cz6Y#f`*Dt~cHPM0~S> z`H{Tz1Pp~mU@i2@`dmr&;>FSj6F_;Q=zSES92^Zy8ObUrj@($Xz?im!Iab3ktN}9{ zm^6~rSOlNn9cc88`g7x{#;mjbxiM+%moSVEpzV+x5)SnQW&@&)C=Bp&6M~2tEKZ9F zShFc_6}%%X2^z-zvec79xQlw}wrHIv0+x$*Srld8%)>=;MRLHf_x z34qTk`mfmumI>?ItRFT<^=o#TS}r4ARQ4^Hv_>}J2L)lDQ9@a{Uino=pVw?%sh^v=Z(#_?lpWz01;?NaKXFq<~DPELszm>MGFZI~0q21g}D8EllT8pm4_ zi_x-5o>*+mTIG+g3(d=W62UtSCi>-r9A6^gnhny-dPVHTR3Rn8m$?~2S2DU^VDW}` zQN7WIlar5nBLokh@llSvdc>k5oh@ZVO}vQv^Dd zEstu5n0p*wB#{|VWm-tA`r;H|!((FJYw!(NZIS!CXW^(*S**nEk{FHKB_~_zRq-l5ZA5>u;K8xafQbEXe_mSmkS=-Xa5w$0nbbF~9$n@u0Kqs}#nLvm#H;8zS8p=a20zmba_eN%ml0Ta)tOis0{p2I>19I-6N04Kk zybH*gsu_9zOkl^}`bsQh(FM=4z|AzpHbFP`=bIYPJ4*r@{B+}0js#B{4B;g_w4cD% zmq&%jrP(U8BLN{e)87$Ci^S4i8i}HAzt#F@p<+CQAg_!5nJb18uGm7ALWrlot55)hc%?4z<#`xP~>*{O`N@VbI4TL zTf0dLkfRiiW?E1~x2Y>zvz0S^9GxwjZ)OYVq+wbo4v^%mrpTHk(R_>%?YWZQbBt-L4MC6Wzcga^c>7Obyny(T24Y6`h->N<}0x#$UU9K5r~xgIK>+AyFU?U<6|ft z1svKuU@oG!+Q+%fh^cfiC;IQpLq;&Z1Aclw;^3dWFAt$sB)&TVxEud-@bj;@PsaE( zq{f>$A0~vC<0`S0Ys^$3hM0e}JD4jgc4ZJ$AY)yLA?;BH#Mq}?dp_3rALcm5 z5PB^~Qldc_PXs-K(UO?RW;$<=LQF#DFueMv9R5pXkfHlp4%8kQsNfouu*#z< z)u^`IQ_#{xas#vyt3P{J0 zH(tDRkk)Kq2?nVfM`xWRRz{F*xQwO>tuY4mxnJaDgRx60L$%C#q6337<0Q4F+`tAn z5|Ih5o=JVL7`x<0rIO(R5NT~hfta`I zbvVU{Au1h1Icep;vy2h#81eI}rd%z2<_~?r-k3M$-hu^-I;rI~P~l{Qzd`FNBQV{x z^pwY>C<^@~BSl#-^5jR{ND=m7eNIWN#KP085*Gr2ZzU#1ilvsTOH6&Rg+QIQp=^Ba zZ0KmC6BDX>fea?4f{SM(w8h!Heelpmx+N~98&TEABWGCfVn3;JkdY!0qJsXxTtOMJ zq1*||K$i%;C1nxJc}53VJvM`l)%bSc>;L@$f#(6X89WQ3ipVJBVMFkLs? zIoV)NIWQ+G0KlB`JR6@!*;qU$@8o>uq~bL*CmOX2bIL)U2R9^h0-FPKO2LB99+gUR z={3qNM#XCp1w1XzoWRglKFWd6#GJsznwS#=#i^JRT9=Vv5X*K~9xx|%hGtHwKGmnO z5zX^!U{2E6YX0g%x8MYu0{I4@0VA8n5jLZ;NpmJ1VOtpCOfVUyZdVn~(bv}~W&@%^ z8Dd3?oa`57lWNJoBFyFx?1S>}Yv3uzfTs*L@f7ZVeDt~FDRMyJFDMiZy$(7P#ZuPA z<(;|E3VLUwWz7au%*l>YF`0#FftGeIAqV-VfP-K?ipiF-wY-B94A=;Up+z2Mndz)A z@Pxtvmnqp~kH{&NnaLh4+&m(Hzhk+GZHT7jN6SS+M}d!2mTRDszubK#S{5gRXxb~M zSe01UZ)=~Ns_>7oY7H1z1^Ci1HvY5_1AfzzPf)b_O9?R)$l^kbLm>vr49EygLJav= zlutNq25!J7$b_PN0(VddpAebM>6$_)iJYvRo>1b;Bu{}?V1wG|0k6oDap*tnUD zAE+0iGRfoc-|{!iKVCQu{&6Xp#mlE=7Ko+S4p^G)ggN}($|X(AfeS`32RmdAwq_2P zxG6R8kd>00z#NoMm_tm8=w1)z0Er{Q9I(&Wk~vVci8F^kpAbQTC@w_Iq!2N)l@LKj z5@il;UTq462yPHWnFB1gB0@wV${bn<5o9gVMbMN&1bBciN4!Pzfv-35fkPli+`1cl zK&u@*s!QPJ)`e_$C}-&kY>W6#xzk#<%4p=#T7A-_e%UR(3zX5+yU1?J6*_Ya@B=Uv zwbi8fN|bVSPpYGv6(>G6F1x9g3~;4OoEQrkUrwP`#>Z6(aAlx`N&7NM$aO>m6Y!mM zvQG;mb|eaiK$~??u2SyLx6;n&gR{ka(#f@60AYDh!p#$qv2$f7FKZks)0Bz;f!#W= zM+b%rAO!a6z&R4=nlILtd!#Bfo=tKK^$@7V-5XU_Zv8@DqMg1P-t9 zFPb-%n$|eq1+Tj8x#LaTeDwH}kbOwU(>7}xzghhvzP37F_|UYL zGa=`gwtBY>=Scf%(7%>mH{=}D=7Mvy*5(3Vo^*0~(%EWx!evub0%`N;K`4Qc1x7gsjBF#z6DP_! zTIft8tD)&EPs|2QXStpvhXd!J*C+9EwM%mje+}~(`oYD29IA+|VP38I!|hG7%k#i5 z{`tA%58R;f_@nd^nB$dW;I=fbVKFyMmp7@IqN6%7Th*q=`U7LLEnx1;I2^Sdf&|DMeWjx)&M7iWb>b5*Czi zlieiYg(_GKhN$qO`XrEGd_MGjbjF4Fe`*sS!WO*fQp!zoiUt42{!j9up;xsMRmg;R zYMjbg#m$AUsA<+oU7>}R$!r8mrr|HOWh30`X~9M^5jKM9xg8tHwqqlKg=x}}d*&u@ z1KF^z!VU+AoFjowHee&!$!vsD1lR~e6dTD-!A5|8 zp2knTN)sDFDD^8+aIg^uhin8=L`zAAoH))#z}%-|Bgl!7z($yf@_>ynYt2S*Q=qhN zY^;)Pf{jRkVk5KAK+Y^!J+1F1l@}aV?~Q_ zEny?sHrWMi#Fu2qy}(AIl8j~}zBm)?6Mq-I@qGNpSbz(uqoO}s zlbNrg)yP|z)uq$k5n=&jJ3>s}dPhhxuOe@Oa8d}kK%gO?o1N_OBaVu_#wo!@7@q;O}peyFe|*?c|r8+BmQ?nF_4~=?TmO>Dh$Q zDE5J!$%Hb??Mx;g6??tTK;M8xp55v14?A!nh{pV*dWSfN%>3OVv8L^AX*rjtZHk|? zg>Kfr%&;?=370mAPuoUWBXnd+y!9?q=ACy42-LD2nQjT_(E))l(vj(vfH@4v^dSu^ zygf4)E|E4e^WYLw6MryZiMR4Twy>JS<-Db@+KvteloG|9JQjej5f&Ixy2m7QxXNQJ zjKZ7glS;E-Nh)z#=T%U)dM*NfThO{9vE8xKGMhjX0km zjh5^RuV=aeMd^RFg$ty7SNGU8I}EEl3Ip{MCz84J@V8W0DX~L2%E=u(7ahBQ=p8_# z&yy?oCz*$eAA{pOD&ZIT@o}~`;n9b}`#2Y=QVuJ-T2FohRmiewwGOe$NmvWMyNnxb+YOJ*DQIwY#M(X>V#(MVS-BIblR=NkFF ze4Ghm4f6U7al*hn#9|xPOtZEEV~e-O*aho;|XK2e;>r$A^&*C z3}dA}OX}0FrUjNgq+j|LFOeG!#@T+-mzMEalnehIW1gMD=^`B9u&n+y8y*GIhnM^R z*1$O808$plG+45>tGiR(VQ?9D34W6D>vqDKtDS_}B;>TVykL~v@=9173=WqUZ8;u} z32PWy&N>8I+;YMY7fn)eABJC!b=Nir$z9xKXS*24H0#E(3cTkPu~{Xc`Z0EZye;q- zsK*KMdMpX+!H~b5dTfZ-WACsY44GySG{YkCckz<2Dk7*Fd;(_XBRa zAv4||lTb>f{+vq9k4nvUMe1CO)VTfYSRR092DR;EB6%s80GY0KQwT&k`3A?g@%zuQvkWj%yh!SO~dIiIX6CE3~2B9N!sP~1t3?Rvx%#&#VX z6V}?W7w)Cq1$x0f1Ybe~vku|G z_U;dz|4OG^T5&1K$$NnQhv{_R!r_BC)!yib;hOJnxR~OV5|-0jauiv#u;^9`*%(q+y$3rIVrH78`h}J>A}(f zDqw6MEX(!N+pFLqyd4cu1`hF~^zV!C{=R774PjvSq`)KB1}U?X0`Ct3dnbiG9fb8v z3OnaeA8C0~*wcPkQz?%;Os6ULm!hRVCxmwe@La)~nEee7daLAJ3i2pev%RhIh13Ri z39YU7wy3|n&AV9qCGRr%mc5I#|3Z8nD<2tDrn?;71@VY|!-d->qjm)PMzK%`)})|X z4UkEFT+F~G3ar^y7sv?Dt!-t@aLg+7tzwQi;A%$g<}fcstvji1vZ=btKV@}0RC#2q zB%;;j^7GW^s=5*Mt*ToYG-?kt*R}T35aP~~`R)|lwqkX)m>@;QO&N1d!}tQ{f|9MO z#S!Qph(5G=Xc__RXo;U8A+N64OL2Bh<*VlPP~?vx zd|w>l^Fo9SnG&IJe-MDyQ(F@$p6OY2F7D((2cR~*g^Ti03tB;I?;@=*bT8Y4;v=RqTrj_@{yY|SiYZ(&h+QrkPm7C%SpC2M*$dm}DLn{xIBU5>KoIU7I z@nqwramrrhak97mrQ;L|M4xduXzYvRLzRP#Kx{Q#hfK_L3-VD6-FRXS1kM+r#+F>I z8ksGi1Tb(vIAmMVU2rF@XV91fUC#{4cQe2O?q(j8dvPm@$i26t;v)BcLL3PX7CZom z-1|v5Mee-=j?Mc_NACR=V#@ITCL9yii^3LWNZ7(VlY3u@y2Z2v-;N`Fafp;5%}6go za#sm;|AFS+@)P_(crUwi(Td?c-+!6*_)fg&FA0mzkmjPdRB+TLwZ~&|q(_HH8PbgO ze^q-t9xwVaVbK}VT=drR|256@@kAWyu_021G$U=*9=MYAuR|Z-jTil;VbK}VT=Z$S z$CGiS$Aw54(u{Ok?ZLCzJP&aX_=jEq{S4zuJAY26S^$x+c>j-ny}&Vg$2km3;~a-u z!V&4t#&A?37S=EKqEDmi7c6Ubwm4y6+r^2b4AvL}7lIvPEfHJ!FiP9?iy#lvpWh~Z zyCn{jw_D;!zWy?%G;p3q{WQONQN}3kRzVEAKTJN}CV8uc5|WND@)txU(c7|7peKpt z+-fZ)CGwM&`-8Pqa>37!Nd@iHFN?rxSdE__^Ohh`7J-ql-+u6>AWRm4VVGT*F)tzN)b_>> zjP-}{-#-WW0wc`DEg|8eaG&j7$`odU{2;cUrr~4`0++SeUP#-l>RfztzkMlncY_Z< z2y=Y*K`a!;e-tnc#Xx$;e+r0_#VzCaCeA`I-`b~x`i*=Sz(9Hm%L}7PGNjUFpQd$vK*Nv)AcDae*h zqQJNTUM-Pu#x(|wNWCewLi!%CL`0~63b{8!LhdaqB!zv_0_h1fN+d`vm^A+H$ML@` z#LtirzY@{H*}=eLhxyED*6Fa~0Y$F54@;!5k+C9d-N-o{_ZnFqwz8v|xtP`MY!|YV z+Sv};SzT|c+8Mjkq$MZX(A1u2oB8F&YO)nmVVN`Uml*czM_q3<;Z}CJT&6 zgGSYnndMlhM#5BM>l~hn=P(-Pz>qM9NsWzD7xJ{^X=%1MEEHFLmP^udv2PtQCv zVPYy6U+w(@4Pq|1=H2f;*!xd$f|^%*ztqn0gS}sA=fEM}ujy2?^5S*=^o&i5IyNe7 z@4pbb_*kMn;}8$S%a3X-{(cK!knA^%neu+i547jMlW$58sIi)%%wPbm<7Zi0iWYOR z_~SD}xEOjH!VR;I*A=Wm#52}Nt>{DrSu5>it;DHnHCTt9)gG)J3v5irJgz#lUICiw zgIu={)?N_}x+fCk{Q}3LRLZ*B9Y1SQpU$X%wYVE zF~@(|^R>taFMHN5##iBLuwyRy9adgKDtzqorZA9|iU#)79tZ{b$sA_cP9@jo?R@fK}Y`BcJ|$2*-rAp7i%H>NGgc5O)=KVq}B$Y~WBzN<4YR z){q)_zsB1fK7gh6{tyAMq~6mJ088gR0`z_oDZ`KBWq1Y4@Tio5A?^VvgAG0R*8pme z3X`e&zr!7^#!uoXjt@~VWHJg~GAKx@Uk4^5QT+x#c-+NZ4Dyiuxt#Gb^do4dEnV&* zOsl?3)1@A5gW+fsYE`2xN|(TS##(OzH6irrL%nCTQ;)&H`ZrOU!P`+FlyVeB7yD3iUdv`E0>^JW(KF+9$(6c28s= z;h|k)MUavm771)K$|MuE&X%A0&$m{ z#B$!<8fl!JU7fRIc%wCXNcDu&hzv>92rHMBFz%n!N8DhVZjCxSXLio&bYeO0ZjCg~ z&hE~h7~V*YIEb)vu~*&t$7h~V@Azy4C)B1|{ye1CTA)vZT{?=f_q(LwKjLGYV}ft% z8H7$fgGA=8jBz8*46$F8H*dk1~G&M47(;qRd|aQRXiI z7QVs4M~W=PiTXoMtUC1vWR^cB3&y=86!h5jYt}0fPeGJ)*DO69+&@u&73R1>)B6SH zH;PqI?m@m{z^N+W*OLOyQ~|$@1@H|cp8L6(_Ny{Kc(jPO^?wHOwAhyO~n!j-;^zc(G{+-2~owJP3oq#!8*I zE%_Z)m@UCSTE|+^KJN+$>@azc#!{u294m$kHp(oeiZvF&Wkqp~MR7%9NxPk`n&%H} zo`YfX3ac(G!Bo2{`>Z=qp9ff<)n0encv&Z*ex>gkA{uAYfsSqj!cZ+5=&qA21`5zKId09A&*n>lVmGp|K6 zcVrt7M(iwwd0Qhh@Zucbr7eOFK)nOK&DSw_YS#YX`C_YuEhlqqV=6JW(dBZqFar+* z;Cr$q^%7VbOcyk6yO>n%T){LqCRF53ny1QUdtWA#?o4iUF%oST^lsN7Y=A6Awxi~q zDZCvm8}2>PXfJ2IRkX^RT*x+dM_TgUl$N|VxhEjT!tUY2j{C#>?3(op*^0*aDQnhc zxlpoc$!57T`Ge(-t%F~-2G<313eKm|?BjPyk#f;|BgIox%e&Y|&^}*A`_$h8aRcmz>*7^c zdsAm}!mP|F!|b?Naua~aPdy1&M`d<7nqC8qtl0aW0YnfiWZX+*ZBmW3DW!ZVX}V2r z3YcuGGc{o%k7TK%av&JR&JtKifX5Em%PgEw9MdJs9TmwtW$cOm`wH{k0(;A3qF_|h zNCaUymtJ`b%4H;ihcJiKLzrC?)=x-*8N_oBVPCDqPaa1w_)rEdP%v2V;Mf_pBk)s) zeW`s;W=j?wAWFgv91|9<`nT$^BE^b>A?~3*p3D|I9`k}QCPTuQbrjgPRk5vV2Rt-I zNmdRtOsmleuamrbMKN%fN3WhppnPCLB0F~)R%51$h1a6@fC)YHR}i*US{`yyYQy2% zgbV2xrGv+1m(*deV~?}Z7UPLPSnS9bdqnbu##z2lip#f~-*r5nK^N>?`Ki7b%*R-) zRAYB}r~;qcL(!ojW7B&mA0f!ehhTpl@A@rJShQ*kd@5mF&II@~-Io$6r;s`#+g=Y- zm<@ia*p|1w-Uy%wK>t+a7D+$0OKmFro@$t#p{GY84Q>flAsQYZ`Q8R}m^U@g~Gn==WueNRou)B3gRl zSK+8O_4-7asG{_C5IzF|^0tH@Ud1kn3SQ->@cID4cuX5GOVqNeZp+RT10k#M7c_)f zEDh5*f$66n`WMu(U2l89%jV#x3YC4(t^(*MKsTlU1_;nCD1i9{=w=Jr?Gd)y#k$=( zRl6}Z+Uo6*cDrPXc4LyAwVMiW-)>Ss{Dkc$Dg1Uz8gJKatf?36;gc;|0 zv~dQ{s^on7l98(j&p*SW>5^rZ0P19KM*M$6oj2Iiof=X%{8LOHKRoKqQ@z+R?muh;PspygE zK#yA^^my46^vEPTqem6qo*o$it=9007!l0CChP`)^PzJmsEF2ev8PGw4`+u~9pk6D!_O zIm6wX46Sxf+z2N*%A_CC~T zU?ap5cNU*1QBhH3GHNDADqWUO!H2Z(Yf8(b#yV}uV+vKLqyf#paTVLe-J9KRR?k98 z%$RTB2Vay4-s+pw=WO*kM?MqQP56e5HCU^R7-v)u!XngAPoJV7A83m5^5!V+`GQQf zZ`l_=jivZ;m+|LH)X-fHmyrF+w;~~G`DUb&G!3SaaJ0+P z+B;DRNfHUX8W{N4_`C7FB*h~?G!5?3#{t8L9+B351`HwW5)5+{e|$e=BmvbGaN)qz zDICNgqe;CWV>p3?hVc#7QFxtbON^Yj^YYsvzs2OjBX9leHNXDGIR_V^BjBv8X6P{& zI!$|%gY||iLgzu>!MQr6=4us5B^X;z9X$ipbRJF0H6@c0aY{}4v8d~FMlHP()}f+w zO3SZOSM3j|Y9vcax+Gy$W~B3~6nN5;7}t$R3j4 z@eZSQIEq?3(ZSD~@Ke7MQy==~6YQH2(#K&~-)P#ijK|O~-(p$R(ibITN{Z`3(C2{&lczNBiUy{6o#OLsnF;HY?5iE9tDb?N9@Dc31OgOsBcg!= z$ZT~-Q(UO>bX>7_I0l9fv|onah-VbJ=6F3)D^`ND-s(W&z;Dp2llrs44#H&11fXQ8 zU|RJ-x{!7kBXr|B8JJkbCM`A4(0x93yv&C|EKP9_P;6YcBIj`QjIXA0f;SmMbx_V% zl13?Spjs01Zp1_rfN95?Ad!evE2|D9QZ|^0>hcdupoL6X@*pbn15ueDNXh&x=0~#j zDxniHX%hBkn1S(RJp~yL43yEqaj~>r?Sbji3vMT=?_cYI-6=$Oc^H|#6XS}AK(&kx za-i>E^;yjiv%bq)6FLtGooiJ*j$@=cEDSh2bRH2ppGRlO6Lz4`3Qtf@VGPPC+@a(N zb5RiCFv=-xMmdGol)R%A+GEw{IQ1D-pX2#KY^VWV89Gl0oe&cJI=(J+LSke|tLk%x z`kbjgHGY5+P5MUdykU*^Chdf|07M?YQy}&bPlKN>B|w%u2uSpe^Rwk9j1`%&Ocw)- zGb`8m5H1W@r8!3ZqYxY@VO&J2qWt3Jw1n}t0Fu{`ivvi9hP<7Svzcn9hFlWF;R|QP zd}#o|^;`nk8bAsfa#;XzG~^utWR`|(3m|0;xjcaMXvh@-1n)9SURM(Gdp1I+hP*R~ zGh0Kh3Lqs7c~_9CS3|BQL~1%mL*7k@NNIRCGdZ}Wh$WWj&I)i$SnuMxiVZqA&Jb@d zBH5N6%8)^m=4Ot!>PINKExyidoCMzx#yaV!0xLfVIO2aT{YdyKe53J5WYKu1>UbwM z#Ulkqz?;kV-O|Mf;5VEB!PIXs@RL2kxv?ap;{bAJ) z0t4`7C_3CsE>gWti0_q6#OD)R1TMq|xDZzaF2n`65Yvkz#6*@K0!@B2C=c!RqArrFvbXkb*?S; zKpKi1oK8ViSXZP*uosIEk`QcLu2k{H;lqV4SR56>4MB5O9aEe=z7{{Eb%u+_svZgu z^x~@6s=TV8RR|hZ5%OZPaTj?Rt3}Tj=wY?!`64~6mMQW`F@-aJ_YlU~^)JkKtivD|StIjtqXC*z^b4f*@3IX>xDqpbBqG`3B&*1_IWsMit(pMSBG7uI z7r-edc^l|%@K-HW7Q(I+f1g@C!Nq4zPoF9l)5T(a2(i#~ek{Q&EKr;ZS^2{_F zuR#p)$d-k$iuFuHp79Fbv5pRTOLYP`olr$24(#-DQgG%TZ_hDqu{EQGmO_yt^GCR50OaU zZi)nC=#v2>U3)!<-fVD+QfoafhO4+=i|;vtgvPQ74YS}RAdW~G(JL1MC}7UOaSrwG&zEl zN025f`x+7HT&bPP%P=Cz5R~>S4dF7gL z*o*DTb-;&E!S735-^04bnhYYL$Qb{GZ`fk=sdX6Tk(aA5@He(5ePP(kR2WExVf2Mz z8&wzvFT>~y!``66WXLj8dLbdBn1NSKkNk}>kA2NKk*y5g zqncYBY+1_urg5vu?nlkb-uVC`(gpl1fF;;r-dn`^(3fAh+Iy>pV$pG-IPGuTc%XNY zIORmZ+jIoj9b-kHRyV7?w`=Dw5Am=D5aV1q-94*4i2HuBcWCFDH68?f zfnwnU$)3*f&m87Kq!;Kphk20Y#d+kR-aEyK@*eD66*}LgBcKeB*Cj5T5Y)v9yw_+a z>h>NwOCBUO1~MmzXyQEJa1XMXB*ALydT}mWwrsU`gNC9glz`cEM;_);)HN5Ja>`As zy&J{((3iiw+Iydl01TM&nG>Z?v*zZomi_>p@({N7K^6ERe#+j>{IEw3^**efR~_n+ zCj!AoH3VzTTSDipq4T!TiOE0^A@0XQ=f~+Rd$(%{T7h#PGsNMHPlirTg2?breg-Zo z0?DVuN!^zBX*z2)3_R~Mq4TqJmc7qOygRYa=6#+{phnN$6+*v2XUY3w2)Ucie%rf; z-vwyAgS>m`bdMsdQ?&b0pdde_suq4~Nc2v=bN})y^Z=djCeJi`K%Lu6N4|Q3L2T zXBeFBen4L4ViK`0!8Z3Cku+~sq8g_Y?0iCCl-UGM3_?s-=KHasK4<)BqQMwW8!rK# zr--M%oU@cw{}-@?!EsDk$+J!c4}chO3?kR>0~qdO>E;>~?w3a3SQROFbw4R7aKoS; z{Pu^$9W7HsO4h&>XDjtqzK|F7mXcPWfLy1v-I9GSUB%pCS6_x7Yva1S_F~SUT6_7S z>o*1VPU|{wJy#VK-UvfGIAL8wXG$g6Jc=ZuDK1*H@)qkd+C9nnT@*6qSP^9FX^6J` z&~>2^8Z~HQfCklMMr_camIQA`F2x)~3s#F0H?LK1P-$6BUh?%gw0V5QjwK zMXY9}KWkmWGUE05fcmOXR@-W7QU~yG&*AFlWAe4|urc1ug0MxJxEeG8Ga~9nL08<8 zTw^343CuOhwawrFwTtnCwjW=ebbGOu%#XYlDU4F>1_s{N9C#E16CK%=@52{3*Ko8n z4v}9hhg3I*g>2kHSWxB$SzW|Sb|*bvcJXc%mq(4&0F|It=2_f5%T7{A0G~6_@A$C$ zRV3LK$d7!Qg+=+b5u>#*wxHYo-I@e%U-JU_=sDadIfUUJ)rmmF8hSW;z#+qdk0`HHs6S@6Z zJM(M3pZIYP_Wnadf#Ih<^dRqN8v5q7-p}a_xatPKUQxdKDgZ;idLPuqCjz2b*{eh6 zN#cyL*OLK~cA8_H1_J#L(tCYh#b1AgVWRo#CdFS*L2RGDzJ_kiUtb%6pBjOm7J+Y! zz&AzUuZzG>kHD)D_!$xSnGtv`0aT? zrx}cca}E;&QQT-{SHsC_!FOgpf6Y27*iq)$GgIY)tMD65xj4?O$+;8&40Ts(xJV3Yv$gontYd*@A!VBr*GMt$#r!zBCeKX=fae{YwR>?h zElL^HjA8tY^f0+|Gu0T~h>^cTDCeZqY{G^O*^&&d#k=H>fQLx>& z(T785e0o8;hKm=Q-m>z(U3v2j!li?_kYS8`gJBAEsSlD^>@n2${n2FQX?Nu0H(?n3Pv#i_tIHEj8Hl#|8y2-rp#z!l&y19 z!LwBGtT5Q!hKvRqm*cnSG_K&MfVaS5=EZj!P=6CWSP&z96bEgUEb=u+L5odyYMIG8d=3u1?56mAD|* zj77Z9DufUNc8mGAqod%JRzd2N*E>Dy;E~Snre~D-uUTL0g&bILdh72*Hr1<`7jEdg zU%EZoC%OwrWnrI;ZPG=E^;;N+rWM*cvxrB<+R)rwXD-Te``YDrK2}QWLY+wRVPVC+ zWG@|M#oTsLP8NkH6f4Df&b*b-z@VM+N}OZ~>~UFNaZHVh&BDPjIhJraKb8Ah|CEtqfG5sjz7gXu~%@^a)X-5uZL3#J7sOVx(9s10AGm!AZ`rGyCCUMWZY3UD#Vjd~pbn z_o4*O(0ZAX^#oHK*64HwPQ@8kY|j~zVlU5y&;dzRtcYFK5@>1VC}A;q1BP18~VU^9SJ&yFZE_HXX|5obg?A5AkNeT#*-N}@!Ml@L8v zE)L!eI}{}xhui>@%;5a23D_2>Nw~uO5tK7CSq=PAw>nv0@FJA{Z(m;!tg5D|FSrhE z^X&8mGy`s{FF1yQ)E9h!4c$&(5EN4B3qmi*IO&Gow)z5Yp-o$5@Hv(@WH93LRR-@> z#XOwL)k!LYupl7sXt_Z6QEyw7ffVz((i+@>p+l{~o%~Xb0G*`(px#vgD0c+_&8`A` zP6Iyg1Mbp*FZh5jYQWt-;2sUQ*9ZKI2HfWZzT`XZ_Z?8!s{9`C9S{1BFZ+&%e8*Sl z0PBPu-xd(d&%s#gUj>YNs}JLgEh8>D%l_`Y_XvQ|MMeEl00yx$`fvEwzlI<0-!)=q z5*ze+L#5iLcsv(s8QUmzh|N|pi34yWH<6%>boFtDWSNrvzmHMwsNIXilQVCbD>h+Y zP97|83d;f;>{tv&Z7htqqFyNZ7+q^7@IJ&0-u{d^R9O)uYfmF5CncCM{ojk~tCBYD z?dC>~jqE`_VjNzY@$yAjmD7%!;`#qW-x!HdtWJm&sE%Ml^`uTS4i#`{oMJh~)%aa% zRh0;(?u1%DB@n^YX|VUmG?#^G=u}8lZXzs;EU{k2QwSqqEt^Nh3n(5qzgcR{3=$JQ z|1H%{(4)yVgG0^50)u-C1?8mG9(2Bk41lgFI6GrIMPH5uhYNxT5tg$im>3xasGb-98s)zdlV3!2@8~&cfFWC3>B}%8(Pn#RHX92;S zdlpY2k*`zxA>CT1_M-^=$3DEeH_O0$QwGfRjsGNWHKd6SUG@`19c)0WXy+O~#cy6$ zL2A=bzpPHHF~A)S*o*xP!MNxRznV5SW!G(_hM6E0JO)r&EyO7LWx7i7lb`R>}jRw^?jS`P&>K4{#>IPDe^e1|Qk!Na=(-9U##Fo%2VoT_47F*K(PKOke2JP7FXr=p8@l-L}ZFYzxO)2F+F+wP| zaD0l`@=FcixReVoJ_Pd*+#oUpW2D6ssMVq1ZiB;#MHS2a72?p)Xg`2(rt#PCdWVtu zp$Pa?{|kVwa{V`iGnv|Z-p1C!pSkc9`TG%!-Hrmq)bttWcl*)n`O~ zR;te`^*KO&4pg6m_|Y@)FYodWrnl^^jzia|xQFl)T;GB5ZW&uqwj&gsGmWBiv*M|$ zH~t&7!H(VOM(xN0VLMZMvd=$S@3b`0C!d~eGIiaubj8P2 zSFH8BVicJ)b;Sz+^1EUTZgd4dv94e;Ou$@cLCY_Hk-j*RslvW^KD}KY@#1ro`n*tm zUZg%RR-c!s&(Z30jQSkQPs#fyesnW)4$EUq{|aqfzYA5urHG$K`vn%E*mgJ+jclX` zX!lV4E!|e{rE@SD4VG|N`87Z`hNmMK0JEC2sr?KQK~6tST%h zu%BO@0OYJ&&@AY@XW-6f8b%UFdNPd}_`o8eYVwPHb5_~aUC{N`)F2CW3^kB+RO70b;Kl|I*%s%Po5)F-Vz8THAk z4J^BEGfEDUdkzjK=jQQ&ts^S6$!wg-PfyA101(^v631sLs~4`k_ShXqe0SSw*hnIt zh8srUa65@RpRLctcMw_3!qBy1LW4pgPsC2)FS6LBQgG_~G?umZ|&JvCfiDOI17hwzGivFZ@W zZTD!lQYF1A)*&ZFmy-LN!Bj2QZFK4l88&Fq8 zx6Ke-s%?gyVbwNE;E=Z2+gaiat%Le;Z+`Z5_6ZweZ)uyQ&b}dN3EO5_QzW#+@^~ab z_Jdk%X%$Ne&{5i1u3Z^tnRaEJ{j|$=h68h+s4VMmGcHCmE_N0>d!ZTUq8Zb)(lJ)A zD|Tjl*Gy~+VyOZ>_$-*gD3^mprg9aWtacS0TlN??7Q3V!8nEV)Z&~!_qfa z>@gc}K>4BKTrqU0k#(^+vz^XO&dg1~H*iQ5D>SV6)x(}|F(lI35|U)Mx-j|z z-~$EqINc)ml{JxpkX&KN|Ed`BNo2^pOYITmKi&6#OZng6`@g6Bo?_tS-yS-Se`MNV z&!?f>+Q}{ePu`?H*lUGt;DIQcM{a8l;2^)@k?ENIZ}P~iG>H6)7;ZSkt7#_|rS;Hx z7M*2}E#zM={q2Mr46TSx^;=|<2FI9?bkjr<_FM@Ax@Y-gmRNwFEBT1^=(x(~Jbt#E ziyDh=eG42D*3Zxma%g67oFU%zjHIsfk@aWBfb?nn22D?+DBG&N5EYlZCWi;D+OfW8 z6>s0NYNLVw%_g?|u18Y5cnp-da}B3O{=@<*Ky@LyzQfEm_Q2PBGjq*02I)RuxfjuW zfpYIj_gj?PrTeYQy%*gV(%raHeg#iMFf=4>LsB&)euFWkbE|#oJ_lB|b|T8$cPWez zA<5u0JwS3805;L4VA4K3uTG5G2f`+1?ZcDw%t8C+n39w7xetMGtBX-kj2XvlNR~fF zmI^lVPPCKRcsEj`iDW^wlxr&miZ{|lh~ziY+mu@x>0;%UMtZw)OCw#P+|o#w(%pEc z{0g3iU}#9%hNNmp{03vj8tJNCNhH_q1@84+mu^c_HyNx zmc4@R#=GQK@H7NNL((=RRYT%87&F$g(nxp48i^QmWzk5)q8k`Myn9uo`&6X+`N5t156Cy=eObPi_Z56mHPkI(VDC=;fGVp{eU7mb z-Yxtjz1#Rnc^~5ksBV{U%KM~zE$>eGroB(gH{*R4Un$oNh3qE66W)9HNqX1vlk%?T z2gu$l-;{Twd@b+&`1<8KEG$=#LiH=gN_bE4lk|SXPs;lVKS1?U`KG*|%h&RLA>Xw3 zOMF2CNdsnI>i(U|OF;(P(Z;eGN|&q93GlLw>i`nE4u7i96WHqIM|J8*lbIJ#o=JI} zYSsq3SQhldH$>5w6nd^flE$7W!|#NSa3>KtX_%F`LW#t`?@G;wzh@en^p4;*ozuaI zO%Y5^HbsayS(44d9rp}MsGvkN2rMLFsvAEmMq24gI>NHJD^N{+oD$_Ujb4PIQW!5N z*6nN8Z*tKaHYLH^*RBiBU7E(ei1$BgTtJ6nC-$B%3NO1pLWH=`b5rETmlr~pjDt&R zr*3+dn}IW} z7LA|4WSEaEAUg_pi)AH>=CXWSR^g|50Dcs3Y+wjUPs3I|HDHGswM)^y9`U>3*?DxA7gv z<`h$IyovUWJ|h)hJ#6LHZb)LKQc#(@m0s;4=H7%iI3j=&NMC*AM2)Xi<73Y3j6di! zk50yq2#*~hV(E*0iyQA*dyC;0oqZuSzb~YZ>kHBzKT%)EO{O2EFC6z}Tc2NYyZ`3r z&rQCXuOFP?Bc9;XSI^?>ah?%>MeBzrqH(TvQmizeLCezyy?I1qhr$urAA#8Xb1HTE zrD$w6YKpnhCCo_H?kaoNoU;XU(3yh4(T(eav-_x(4cb%7)*S||C+K8Xt0i-5HfVwu zrZ}8EiJ4k+>21W+pFl>s3Pab~hU=+Kp;ND@M1sn6{cO@j85TL6xOIn0hhIJX(Yt) zZpdhV5`B?7#Vvd7oRGgoX)@8D$Z0B2KMz#8r2PbD!sxAn=iOb@w>kegg8!-WpDX{{ zo&SmQzf1n6aqh3Fk5Pld(kf1BGK{TZ%mr7|Y+jq%;V^J>|~ z7Mx;ZlHXk7DbsjfkS6#e?whtq)VlL)B=XmFt)BkQuhrIH*EJIPJHKrbS;yRd>w;A3 zaa4j>Mx=}*QnpK1Y*lrc{m~+Hb-+PmnDunB#x|{~O*PcfX;k!$HK`fVPM2C%m~4SA z6(PB`BJ7H+1(3~Z57|_6k;~$b`wpq5TH^+}mnv%Qo>r6iW2gTf;<;UMyBxYQ+xx$2 zd*9pk{CY=w%44)vfWxwGcEubSt zV*bej-hRWs5J=i@kPf-6m1a^l)%-m1_B0gsl0TtJd(fwtjC#7}+fF``6LYLZE{o?_ zZbGv~mx`TJOmz=)Ms=K@-a^?X8caY{oF`ita-JL(86wqQOt>n}Rp3Z;;luFIc)5s-*zSXZL0YXWJ-Yjt~t18xvmOdOLD$FCb^R@0dO;%OyPquGj zG+s|ycLM_sf%{KpY|q`tUliQ)Xe0C#DDf$(8UFFCsvvS}xr)u)i4 zixEKA`o!Mp`sD;Ep%vA5pPHJr4V)c*#Yv}J()QQ^!MSHy2eltJaGZcWflui>ZeW!w zt#ja9oo`&+F~QO0J>A$6&UqEy;GQ*T(=`5`npBNl<6@ym-}nzf-Yb$q`VD)wK{?0) z61y*$gPzETT7Lx1Zv6lrtplRF#@Yn_6m*~6s&y|{J%B7OT0*}=r74s%7%i4_STNoN zc?uoTjh|ewTu7C_@l)y&>8qt0f3gzq2h6)W}6VxqsdMBSZ+!Vtcn|m4@NEUc+?{nCABK)3_XDjl;rd{fd#;Dlz(J5QGJ9cCMcb z64K<-e(L&tGhA|Qek872*v%~_8wDk*Y}qn)2c-nZqfcioAeH6aU&5}5sQ7|I>XBSv z(&-yFQMO~rkhyt@U9u_-IJwlIbIQ)CluJXCFR^3;oJPM)6yXk$-Iy@s?w-|1IDGsK zsHFgPZKkxWADK|Nv~>f@{E~TKQqS&L4Ix-qG0C2n+6;s%TrsHO&o2h?(ecHs0%*mo z?ninQu4)xy{$w#D;c^5|YmyhO*7`|ntoae@Q(D(}BcCQ*YtvCU=A3m4YWO(eI-AF0 z;}gm=tdnG3bj345e~$cw-7Py_z7y{K`63t zm8%_94XZn5i3$gerL}GlmRo){>tUZ=L*moix_dLWZPYgi+nDk%q@b{mx;8G zv8wW=qcwj~wYfj+ImnH@zQj7fiSq}^Vf=AvLn_?hIAohg2%Jq{j6O>O#TbwLuRdT* z^1u0jamh9lHTBCzSv%QUSR3zQv~0nr;Z#aYxKvZc_z*{s zD#wR7%M?EAGPgN$Zak|^PT}OX#Q1h<&Ro*8x*Il!#hPx^YX3NLh@b4=+<$Wa7I(rJ zZsvBy$)%0qmeQuiWfT!kSQWyJ&e>#6)q3*{i5fWGKy|D?6_mOcu>Dre298H?)vr$x z$gRMOGK5MOQ%9)efRKGo6(|A z;&1#VbxJkmbrt)H;MKQQV<$+oAB3L)B?j`Lys~5d`#b2JTLEhjA!UwKXF`r zf1!E@oo00I=A*&2Q;{|#kB8F_3*9MrxC!wIfmxhQH9s8pGfgBr!X0kg9XNAsjn!v& za0JdU@Xm0jncO{yKEO^hx_c0nfSvBJrL-j#Zua#**;jqbng2? z(LI}&rkmorT(3z)*yBAMT3^L56I0iifgXvTmgr`QK4w#udxF=cg*{St>~U#&(*trSr!A*~6}rd{oJM(!m+D1$|@Afzs6(rV1*-(+}1Cc)M$3jRy5-+v70H9_bs{ zJT0wiad%pA`3)QNaKO{Z;$%on4#CCR@21>~cg1wh)e?qYdeypD#J+*}s`+zhp&PfPW)@m`MR9h&w(QPM%(+%w)b1|HkK7I#r=7ux=-Os z^c^|=UzU@Nz9Q$yQo4riSw`?2%bl+M!KwU=%_*k3O~VPp>(wuS7N(l> zad%2x{~~~O8`kL{LzPwAbyjc+i_~^~hl+Na_Y%Ww#90CRWB8qSSr@aCn^v2qD&sge zt%22-^Yb{8*=8{{%O!$hq%vGdPHXT#gu($*i9+gMGSm;ZYS4HgPuZP%1UAv z`t+WlvE&>L5Y@ghiMLC4RR(HVcUmAtPmk!a)4BQ&Sl!D-hJVf;sDeap>|=vC;P4(b5XWmIN zW%eZdPi1k`v(8m#=HzQTN*+(gMZuZ7f;rJq`_w~e&ipk81l2E-EnUvkzk*xLjZNg% znQhd@wqpE=-IgNra`k&jjF#h9akzcNDegHeYZ7OD4M^|g0HH#C6aMQ?r$qL# zXE2bY`Iw59GnlxkS^qk*jlC3d3FuY ziYV8=+fG%zAAbq;b4@Q;cUTyoB0KS+vnPCtXyH@9;p3H=uzRQjSH#T47}nf|R-^*0 z+6vZ*9petdu80_WoGC`ZJf(T;0jggeWOkmaB23I(!?AAn@GbT0nEzLDpIb};#yiF* z1TBx$pTW#8Z!6K#>H3wxTu%@wOPHBetkIQkU?&jKx2cI+BHT@qun3DLVEr^|p*~@! zU-$ls)33q83t0W^EMUBg1Av;SeOFLBtfN7F(8`p3S0CWxpshB31)oieO{)(a?^$__ zaoH}^e`Yjl^~voMdA||V`CS*H2j4SQ>JJ)kZ4KMTU3kO(6U}drBcTn^3@{IX?+bwY zgE+!G03L_|<^i|PBj1CoKUQX8!!CoL@QTm~$R+4y%L;TI8IRoHnSjAQ1`+IADQMhBmuaj%mk;z0>U6bpQq^5X^zM9R`Ux2|n}0gC9pK+`{?*oEnNZkk zOHCbMW#f>Bx_WgEA?k7 zKBz7BfU*aqnxbKx17WCgh&&xR1c-Bp7P;ULTlHz`lf($1MO@_I|6GwNIIl|~hn*(l zf3p1kX5VEA_*3NnO)(-*!Y9)G|5I0F4XwIze9@{a=M^nDX9SN?*Z=&d)ToHRMB-C+ ze2L_z6kXYXwCcnTq*Yh87A-hCga0e)`t(Gdo{^|4t5mD5jDzFrnht)SzR=Cso!!Nn zq&wYfSY>kv569$YqIGhz(Rw-Q=p-EU<>VyjxxQt9I@LInk)D!DM9y({IvA>6LNKUp z%ax<5@bFFw4i+B)`vsRdI33jbQ5@K<75X;iI)(Lko$ITqUKPp_NAQ{$Bi`~^r8^-H^btE~-<8y;Wk#B8>qk!lZf zaUZQogYzoe)4q{-DV za*iLL6Uv<;CPBG$i}{RcyE$D&E2`*LjD^uU1987~f;r0ZH}DtsXKdJqqJsD#$88kltwhQy$gR7h zKJUdAz+&&kaPks)y*4it%iTHT_H4A_LmKa94$5Thm^RfspKRt$%u)_2 zW^}v@0~U9GjH+>Jqolm!#&5cQm1B7>B9N)SgLy!sz9z*U(&*2d#d>)!toKaE4_X-g**Al80YIM&V}5W^S?YX zW(0XI;vUxGe6IRWYWZDZc7x71QcN~KJLbgrdPm^zXpgNZ(OK4YnU_*QAEo7N-Aq@X0|}fGoZENEzLJld zs(zS$;v05t|G)q)=x|r#n{3sz-Pf|}P=3Y_8>CQn7rrIO408pU^9 zGr>Np7Fr)qyIo85c@Sl+c`Y?Zj&~5JyLpJ;9v!u5b80Q)mN41{7ekl)(JtO>2Vn-= zo|)h;z{gQEJO@YN&j(e}y=-Ey!E6c|-RE)L!bYVVyza$Gn`0X70cs5>2D2{#L3fbS z1B`|KjGL#h;-S5CPi3>Wi(%)Tu+MUsskfa~t;4Tg<>`s&<*j zz2ug#6g}1Iu42O_m8n$I`HqGq$g-r2DvE4>awkC543#7HR;F)hD)V$S!!PAiL$p^^ zLu`TWI7t6?NpCt(VF*>i$~bF)(3Ed+ZsnGyvRZzgJj3hAcK|t&(j7TXGnf?MZPbcs zqb|A8!gPK`T$*MBG;5#U3NsCGiCa~`WK8qaL}^7S=x{>C=NSr%Q@Lu$jzd>bjhaKy z!N@&KzckO#(P`Z)<#p|7+M3czce=y6t;4AQ!9qB6boD1cctdS!f2z5YB#zU6T4&xf z9y{-jR^BT*^ZE)jJ5V;2sLG^XjGmrMj&=W`qtX5($RJZdS_(7jAS2<(j&GwXJxa0-OZSdRf_AAqWE^>ODwyP!Dl*?KBr+gzFXKC-`Qc}$}=uX91m>*se;H?^VsM4ssj&S+p!Cd_Xc}et20x&oX1wYR^|2Qj}6KV^Y%KxMB(`gm3Y4t z)zvr4f2^$E`t@XF-BLj)C{NtRGp6+>uys_S!W-)v$69zsf;`2vDrhL`hpisW{R~Fd z!IVr)=px9D^$f={N7J0tSD!(pT}n=&^?+>A5NigU_)5Nh1RPG%B;I~&N+e;T5);7R z+TBsVKClm>kj%v13U=*|1`@#sc144o!LovUT~077-iz6-A@iEqtzmh~(TJRZXsL6S zIcKGd6PHJlRSvk?Icpqn%sK1iSSO0sdoR{6sxfp|bdvXSBYHy&4I&n3W&7|;_C@3J zmZJ$D2@Ai`d#U;+@8!9h&E5;5ljF2|qb=sO*LSv>*V?wtC*V%>Dc(!tHDRc(<$aOn zKzToLR%BBdUN#JxzsxAvnUTh+_iDH+Ep^c3;Jq5_@}9dlI@5dEi0m@2)nT{1N49rP ze-~={dzkS&Hm8_qpAwa$^PHnaF*W;Ax`lDjso6K{I6-W(N}>nY`e!A(t!Oh&Xn}t8caW8JDOSWOI>9_4ANq3uT)(+^wkeWAR>#UAoyh zXamo+jH-x*%o}siRlfw@G>#KL_70=h?#L!n(PMXH)2;K`RO*k`MZ0|PZ+SF6RG+?V z*-*k!JQMsdD$l}kN~XO=Sgmu<04C*8Su=(iRM1pSdu!}zn`;SPhr;C||3>zjaE(1A z*o*raK0qlG^yP;(=1s8{Qqmor|%zICb@Qq>vwobF6{bKW)& z%!UVH(Ne51-L^rX(eu{ANF+&xBFgpq_VdVdBvLto!I(f04fJWE(J(VR&Eui#zhk8_ zOB;?s*sDooUAg+}swC!`by)9&c#50c!!F5!F0I=vukT-P`oNOJwPwaG8{&Y>+_~$J zzX8%i)6VWS9e^$%oi-_aqCPslE@-@g9-d&l+qs;o@%}@okR_^vSO+2{*jOzyvbM7Q@AM5D=?mKC1;QoP183sdDIZTvu4=9>{uG6_ zsxD;U4TppxvyUF>@_A9FQ#w~k7wL{rUPHm|c%ktYykWmZI(@?Twd201xE@31;(pYM z)BV;0!%zHXE$_!6?|l%oZZwlL)0t{&(Vi(S?aPc0X-H||-p0O!7H~CA)=b5>NF8?- z7JYnz{f3CyUTeFb?ZE*{JY4-PJ~9tD-hi`^p^<~vU|D}@xNNGlJokcY;6gONp>#XG zEL`sP?{>gVK!*{wxXfVVhz-QEDa&eAy-=t8&1(o-Jo?=6B!aQ9j#Q2L)z$IOyN z90rI4(^g1nG;R13WZBF;GA-wN!C5POX%*W=*QU zGF8Jy*M;rlwQy6JpaYyM!6jSn+kdQ%!^6>ptFI$6bnlSiTqojnQxwm|z>nuDl}i10l-+k$ zS!;J4Yt6ZT{=r**^o8rsWLn=AZsQ3H30}!Lw<|n(zhRguZJCdGPgUH#d%x(Dkh$ zt?!xo>&aYnPPduy=~**z*0S$@_0*AFI&7KzEu&NY_ehqrL6dU72}s%?+4OOs>doY; zzkwf1;S!iPiK=>(1Q|=)%|Wa})d;#5psIE}h~z4HtW^1}MHgV!`5n(;W0CjQz;#}C zHWlTXjqT;KRW>d;v@Gkt54Lpu7Jj>ZC}kl|?yN)%wW)qx2Gq`_l;s2~YkXNccqTYU zgtN7@a!y{`8DM&Eb&%o_P)#4UruG(coHghKFcTE`d}eJgRsRFxT{9*=9po!h-T74g zjSi+g)_Zfwd+aG|gVDPjsiS%>*T$~na>WREqr{q5l;DQFk)|AZ*$QZ0Nmbc6nt)yR z`<~4S7CX)aFLxZb)tzfD#)N3bjzN3k0LM5vjyAL*vd2`G(8shNs{bJk;7oA^VdL!% z!P{BCwV#4s8;1h@nY`oQ@(K1WZ`@k-k4b^m+%W6Ps&crPE9HjlXh|wvw#qFFmyK_1 z3|zzt;+ScFAJFBo`Nf?UzL+F#?$DMs!T)u6{;%NqzoO&+c>bx>!_hpUDGOVz`=d{=pl~z`qrI9S}PrS-&?Ru*nV&kla1aT6_w=h3h?on=k`LkjV*1w7ZhP=rNFjkjjy(lujjHx>XP=-gl%qi3H~YG zO0LeeoSeN#;;T92L7KPYn&;}=*Xc+#;z8|Ho`TC8&!sT8C9v$d`g!E_-t%Lxb+y~Z zRy00A>eyCR*p;kEgf3}k{VmumrY2iTwUye;rg5C9_u(?T{($` zV69OaWVXa{rOnp3QQc+@M`b}kauBu!Z&sID#dfX!7Wvjsz5*y)gFjKIih(#@l+nN_ zo#fYvw-Nz|aFT75S~fxxk&S0w%11iMMvPS+zcx|kx9`--UVR8gNA=ik*Z!(+BU$}V z`LQaz9nX&EGgiRnryEQB|euK{#%TU9WUM5-zM-Qv>Bzx~r3`=z3;c!(l$q2!AtYZOxpl-6&@KS7R zq*@PYtbcNQt_asAQB?@GQ88BBtxC-n>)co|^rlbgM}%J%V6?VNjE0RJu=~_LyDRl` zg1e^1ZF)gGH^hKe*-of!f>IuM%2)q4WZ@9v&qTjd+<4#;RVG};jq&;#P7^$*1#nfk zwgrUjE9qU_P937tVC!+mS-<2)W+DDih*b{INkc>>$Te)CIhbl*E6|{IV#lZ}GF-v~ zx#cileyyZRV;6QYf z6;||@f7v<77_V(ike$7g<}SDmlK3R9?I9tkBx_3~S5ezF9qOnlAzxFT<)Nl$sitlM z;a;p7=f2m0c1`o;qpmcN9`%F!xfJWEGYlPC3Giv3gTdu?0(Qv8ujrB(~FMJ9O7u75c2Y=-b#VQN zjO>Or7Dy28^uFQnbnhDp&+xvb;hE0o`Ih0vCqcJBgT$nzo{9obqHl&6q}@HUKo1r9 zZcFeT3wBX4Adi8~Z?HGe5OhmY!S~WPzak^m4*XBfo^^B9{F0r^Cgn2iN1Sp3Yom5e z8+29cd6eV#JkOWc?Rgq;&*7mQ?jJH_el3Q$wUwRJ_|jJXb|zlrp5$(jJ2a)A3BA^0 zDOI3$@9;!cQZ7;{C{bAznu3w}b!6t4MNy8~Z@o<0opL$yF`FQpXHH_<%_)rXyAzzA z_y9*Bo3G{{;Pg@vMRw`<#%0vBR>-%`ZUl5u@#a*zG05=kPXybxH_{|1FG3ovIf^Q3tJ<;jce>V@75XdaXlWC*W1E9#YAsaA}^xj7RphAx}Ng;awR{{njZLNOuL;jJ5g0K zJCU>{vwImBJ7so)*a$P3-GwKV*$JJWQ<2#zmk$jmWp=LDxOfxSDdC2jq2b%e@06T*EP#QXs-U3 z|mob^&!3m`MQtN^@oWFbN^u>Ii(hVmDg=YQ z!Q|WNAo&U)DP9zKG!EuZpdmn7a`v+9;gvy6=GfZil+vP3F`lXP)>fyKO!p@h8c3;o zNehe(Xi$ZN%3&F%c3yrMu&yoccDQuuu#F4Fsst`w_sa~$xcbBX4qX2yKf!z3pWu`K zhd)7~RN@G@#FWIGln!)~qCY?6-^p0<-gmXVe=hGp^ls<;g>(K=&XK>PS0_g9!S@WY_{eVopaAomr>0tSX2W+^Ft&krYDvs4#pA(Vy355 zTbeizOPqsQWOpuaXAE|>fc#1|vHb`bTZd?gOkGa;!kA9!zH+ciby*!;e8)M(K?d^V z*F>x?T^(qR=ZUed%GMa5u{<%*HPixVv?09XxRn<`Z*{o`F^{3yfJ=Su1$C6SB;XUP zILN%#841(Df&11tlY}0&5co`*I9i!nk;Mv`hFY0q3MQFJ6H6;oE3!DGJA!t&Kg>QK zPIAW*lS6dTWx9+el0)dve>Buzqxb0>W}^4Y$wnWL!`-I8F*h51(A<3Vx8`<5e`jtk z(ubuJ>J0x2MZ`*?b5bGWJmT$mih?*D}OHb z_Xf3T^0&?yy%-Hbw@-+SE?{?Pl=Qj-F#7eUzy+9*EfCIwYX8>@-p`X*M*qGbH`gVd z;cXqNg!bGC2OfE+_tDi~;1t#AeRL%R2lIi1o!&=Jsk>Cnu{YN7rq}zx=Ah|)^!aoD z#qLaQiP!sBGF?KUHL3UU_tb8N$LRa!>3#H})`2Z4ZBvh{_`yhRO4;KfUhhMV^-V@w z^^1fO3LTPWwlpXdcQ>YLV}%Zd-4;XdVVi+B>CD;}-Ll#|SbIz8WVB4B=vi=Sap-;O zvB%cpC=JA-2o;HS6n?{glsE>R5=S{)9Di+;IC?^aF}q3P%FrQZy29BMDiB_QWAM=w zI0j;M2+oKeMTNsBbLtP3Sfyf^Bd|P2Q_==;Q9~{LjX~4j_-86+`WydZ9>f)&mdELD z47c?+KHG+l!8+htdI+|q|1lm=9hKCihh4w2~5feuM|l> zX&w|wK4l&%@)>!KtH{5$p`1onq=yiWR1x3iN=xHrOOr(XC?=f`hS}+Gz*A#v2%tKj zPa{jIh&V8B4n}T6eV3 zaj3LpQMk7He#$?H`wP>5`3U*kWI>hI6~cA?)bFG7@ZZtGzrOLO5H}WD#8S{oECo%< z3fB8C&bjZulRxnZ{jNV8gA(C-uUVrsUbCheAIu15Lw)|i46-tZHLU7r#^p85nv*!j zj}>bK4$&8Z?kv(ZeGj^+k({OqCd#4eT2r-Fjn?ijs{4$E^(CY3TtWhn9jf{;VIka z7&we60!{Q@XGwFs_i8yIf!jV+X||M|r~AZS$o4>azX$4admuBMo}aHdQj|H?7hZh8hW_3*C!(4e zkmT)q9g~J~YQDYQ4f}1HoH^tex1ouBm(63`4o;KCZt>2D(Q+?MmIjiWN?(j&LNgG> zfbkw%J?1_%QyMTnf%BySo_y?zMe@CmPb8nYh9U=1{BVj>szCWTPOuW{h6u|8+mo-+ z+!=Vmui~~p*v!;S7|xW23{o2A7sb4X5cFU&a($+6T6 zt73EG=y(nZ{iqIA?A0-cv<8&MA;p;Ckk)i^NNY~aA+1f)c&%&IT1R7-YatHFr8|VN zck$L89MT4zFQimktKljy3#SIE z_iBL3%Vj?eexFwJfxL(&HF*4jta(~c^occ(TiQL*x7yzOTh< z?919dargcM)enIo=toND>dWukuD-(5%z^5M6%%IPV)-iz=NGOVFF!wCG(-PYG}m-i zXLw;ek5Z3L1dmRY|7@#HOq~B2+B`az&X$dxfHaTYrcOM*Yr(Z5JgrzKD&ljx2I{3)tI#Zq5$ksR(^hCQ3E@Rc~! zmz`H%W)5Ft->!KLXhrTnnfqZzkv_Z3&*UTC!o6+58ztNKX_NWiz%LsD*xq6?G+7Gg zb-W;t&9wwzS-G@)(k|$wR$o-VeowfZGpMDyM;Y4|NDyF!Bf!ek>JQxaa+h@336hSw zq^sb{mnVkaiq?P&S9VSq;qsNTM5BAT$fxohxklT$R(0lLqFs(I>vD2e$c=XPj;#o} zv`6s$k%xSkt>USy9M3x$TT9)XH^XG#n%?{*R$9`|mRz<7e z6XUq%v1-!KlMOo4DV1W+03Ed3-O}9;d(9Rn4wTEKwY6&z35RRb4za7_%*vUWgU)-9 zM^~m|@8C?h_TUU=Oa>k;Lr*(H*=GpCwa|TSxK{QEORzB$uL*PIaEVR+7*h^I?;8sH z%{N>^-Mv&=S4QLi9dxd6UAX=z`u-^t{+(H(&cSw)>p(n$QeAK`)?EBJpE^04p49Ft z8#=p6m|HmfoAJr1wggzIiPu&N_t-7e zuY$YiP5|E)1JNJK<^7YkQVr>D8NaN3AlJ^Ix1o6421$B;=th6n=>6c3y6WG`)w5s4 zuH)kVp&ggjqhH0mcWerab_wFVyOqBU^lwV z{9!zKM*m7(0sN>1_%{HSndfU@z8G3b`6U;9fjDH$-;NVY%l$j8021%-De!Ts z!F{jhcUv*2BHZ5!a98;ItpG<6KWGIwa(JK>2t!QlwCnjfiWa0@(a!_H4rePve^}_M z|2<_#cM>!E52{5e-7<(i?hT?3sP8W=mzGWbL<1Pf&n>0piH^W+*Q@CT5)s+M#4o_c z%pyH~RJ1dNu5jf|XaGq9tnGW&G={aLt>sg1o4- zHV9YPL&7-vVwA84$M$?&&-h1y*6INlH_F^z(BAGG<0jAG3h}Y)dA!P$H2#@n+&Chr zjZXEG3Wn>spi<&dtAA05izy5mR+DfwgXRT{{2*KggpF;u6iA>ord0nFX~#J>@V#5s z39@%ToFRKDEF8zFZowof6iDo~js_&FCG8sbH=AROmoa-S-!-E?5f9p%z6BJR@0a%mHoc;d-n+RR=i9Oi*1 zkBI=7HpiHT!9>1IVA4hIjlz`Yg-vTHFKo)K_?fJ~r+O8v<89T?1C(uq2Q~Ke(%z0g zP(L5BDX#i#y2%b%DoX;!`v_^4v*?L~gnsSmacEgGG>(3LjKLGi z(h7|u)o=Vx95>D}s{8uVU}B(3k4c&o6~9}&+|7-=O&(0%N2g0PWMUtf+SS}jXo0Gz z@JBCBcf*+ep+S$-1ts%;odpLqQ6Qxn&jokCp0dNiBcEY}n7-Pl@f^#&P{ZPjPvJU@R6?!NuP^j|zjl6!Ce5q)>EY?) zr>Q)*x+(=<^;WhQuV>ESF|(sMC6#B|J2O+Riags|>q8IdWRB09%H$giJiuP(8JH%d zql9Oegbc@oV}~_^zMLODFpAN}GbeLD)6+{WIT>f?!6#V^9|p@>;OjJUwpFDy7ooW^T7S{b(jD`(rlHNXF3^#?k1- zQNO^^@Ws)*@NFG*Pf6GiLp|cQG+{2Rx@&U}|VRr#Fd-Gmh#%!H?rCncmWI z*EOuEW=3H*-5uG%lvLlzPrkXG-|5i@>A%Re9i8>tDlDMf16K^=|^Ne~X`Gj@#9Bf+Zd`Q zSFNm}bva&Pc%=%ujZ|Z$C6OX94BNP!tQ>P0XP#^7n}9&qCd60uhS`;iPwq7tdh;}D zv2=6T4vfB!?{;!KH3MS%>5$SCx*OA1QYRUovy7(PgqGn?`GQ^cYa+n`N5rmSsLznu z{&R9;BFOp%JWS)S{}V_iea+cvU-#^(faDgLZbliY}z@m||z<`+ovO z8N)KQF)>YLY{uA!;PosWUFl0;iWjr$ZqNjrrsZb8&F3gpYF(W7Og^H^@fEYR&T>aZyCdYrp&NQ%1ISpMe1k|hDt>5zk)%Y)k>qR(+UH$4 zKtG7hhPex*x?03g$}gwYx+0njD!p7RkeyX48Go2rc{Tm=)p5Ucy>ax@)WLPgFjH{t z>{2@mlh4$iwrj&H{Y&Ngh)jYS>o^+X@eGClwTqxV=wu$ddvYU)SrNjH8Aite#T<+~ zhd`LbE?2ST0rUKvT>E@rY$CgE@Y-5dZ>SYnGtfK9soss>r;n8f$^+HiFu*)d zcNPv7(6uV^A$RYlhwJ>xohB_wb2%(MlaUmoI!ls+QL)H*P*cD5-M1D!?_r}|H=xSZ zXHk~@*x_`kIC&kVbGr;#q$zMjoUCOUe@jJT%Qp_8m(o{2Ji5mRqz$p#T@J?-h1W55 z;`{B-if7P%EX%ZNw0W^*@y6F@YJIjJaS8TkXQFNj@7Kb#=`7#u%3ptWa%#Uz;Z#3n zB1}e+YW9^-XROawmk;lcj@3xoOyeHlsj=XPz$^mWyG z`no(_0jo#%<>_F$4_CB;%V%MocwT$H-C5pE?e-|q+fG|1 z4!ZZ(HBVo}v}}|p*zdGY=yFdZA$9Pu%VqWOnW<7C?X*E;XwGg>$~wNk-R1ku4hbl7 zLs!TieU299bbmY(+)qm;)IL|IlvQ!sp-Z`1%(y!FkSp4!<2h9ShJ|8MUeU}mZ(uZJhJBpRA5OCk-| z9p3I>q%R0C@e^u(ZSSz#vWn=J^y!YMw{ga1VvOh)MCF?L9p_rF68aO~dGd?5v=N*5 z!Y4jXM$wjEIIPDd&=%AQI9viJ!f-_={YE*HX}CBS-pwd-`I7OfellLoZ~_jO*7?gl zwjo2zz^PXIY-6Am$Xa|U*B?}(hd|LEl%t2uU3Q}w&auyYoV3+{=6gp!2d+b){Nzy> zu3+UB$LOS*-7R!cXp`B)k*<$=Gz#!EidV4Eu;`B0E$FP40)LFis+ifCBH^5PF~ zASzseu;Y2mE3KCa!U~XI@GODP;$$E_KIDf9VqqHfdL}5gl1&l+rUN4<^U4j(HlpI~ zFB6r0%#rUBomixQTdU54wumaG%G^ctF#Bw)Md$=NrOTM`5pdE|SYxPPaOTQLqdl2wNr5rD(JInMt5 z&g{S2&aN1jozr!feG49|+SYIak@sdh7K&g-GKANMo9s|0TI9mDaI1??x$p)otg?s| zqcmZzrTWh~aV+jMO3Da&OQXTw+Iu*?yT%wnF;&tp#=`wC;l|dPpm~Uvp6B%+FXJ;Sdsv^ zdz`wK@%WR92>a1^X@xxvk#XTGG!oi1TZpEmP zj8O->@D4esyu)qiNE^D;L6vP;8^iJzG!hZ>;o=A(AnuarXShMQ82)ZaDckrQ^@y#C zbz23!wMD1GyZEY@ZBMy7Q(Tc-qWf)OJjY?lJoa{BP8`d8u^g}DCqfS+fC;pTcOYE0I_c7o>ZIBGxH<`_Ms?O{r`1Vu zuF##-X&~I$S*MO@>eNaQ*J&qpLdYWY)CnPq>crZ!JSMq2sm1uJNOwkf#`Zf>be#S( z6XQS!_(y&~MG`aG>oo*_PQ=)pV(P!Z8)X@&jW_FCo!OM16L{yT|B`5UIOa;T>yc{q z#fy}M-Ix4y>r3{Z`6w8o45&GgX`z@1%jN#v{Xu`B|IF%N(0w!|?+SN~?+6-?fV0tk zb=(IX^N_g+qw+BUrfrgmC0do9VDfL6$veF_13E_XT9T}GK5r|6y|3;bdFqgB!l z)6Wmjx5M-{qWCa9nEbxFa7!%Jn-o)Dkod4WOoVj7d=XLtKnK2{ElP1Bqzl?YBg8a` z5F(@t5|Wl!-O`DWE@%slELTgQggoei4nd60WlV&0L5Bz_5wa{vA&t{+K!kKbhX^SV zLWFcdhXBVbL?lGw7ls$2q!_;qn!zkrcNAfO>nd+!O?<@qs16U zn-d&uOK>y=;b_vt(e?yq$T`E#(Y7OXNLtis9`9A#Z;ulNncse@FzB_Y$CijpQ|EeagYW!HSWR0; zhkQ{Tx?ac=^TIN^C)3nvVW$4aloDu1Ke|U2&26pQS-{Vs)N`lKQBuJ& za97y1U?iuheJ}?T+6GUho5FpC$?jxdu#!SVIQy4w;drussN*PA=?!_HJUw^J#`t(}?Nksoy? zdqX>!VhkBKGLQVYGuhF0GQ~J-JacaQMBDUbtADOzQTXynxO^zXGX?L~!C041gq4ka(4nGtlhhTS3WVY<=0Lco>t}A$iFf zvs4eV;O(t#=(m0NS=pegcsdV_v@{ufld4icC34kmG5Yq z_C}yAQFp3#8m4(%MoFKmCTUBXu(>C5F;;tQYg}90#d(_;KuNn&wN+|3j3d1?NgR4B zf~+0iVxt_VALM9iFt$H;)uPjGS+tlY21rv@F^El?VjcjiV}N-8tcd~U0kAd(mEWwiu9w5 zL&;ThB-3!TF*Av~MRU=6=uI3X=8259@;MP%Z<8|^ZIN?WFb<|)M8|f4YJsF?qe?io z3dcJVIEEaKVTYq)I0OU7iw%bYojA4$$9oevPE)Q_G%05=I#teL!NBnn!=XSYj#Gr= zoe3PPl`9pkkuw;rl5qIabfEr3$%rGhhhz*|ITOy=C?~d7+3r`9qT`zqbnFx5Qqf{LgHf-X z!-7Fa^gifU3Uq#p?ZWYv1de{?N<}3(gHb5wuwdZ8hAlW0h;d|sf1nZQEil*Dzb9e5 zj=+&3m|th!-KqKyV|b7E3*MV9WlVHk%68?Z50!ESUc6!o;Us$oF-4vOf5g*fS}b>H zT0r-%T%G`tVb^}U10}H|(tuo8+|Q($m(!CwBtz~X=*`O~RBTu|Yhm-mgxH6V+Z@bz zi#b~8<>}la1FaM?H-%c~vH^Wuc8?_wF+Qw&_Hz`|W52!jTgo^mJ9(#l?0UCQPM3@r~V*QFL*SwTJjNfezKb4Z?t0CL%-?A3+U$OpA7LOtvE?k;fJ2b(6H-Xg}q< zSeFzjb#Ol#GV6kAo zykV;V_u1tb@#5@~DXngIRLNex1btB22!q+h6j5xS6Qt@#=uX|b7GdIxu<;Tqm9p{a zY^mnmIr0}6jXi_c_BxhWY6P_x&Ay)=i&AEo?`kdqZ*&HoGR!yS=vlyG!3mty=4GU6 zN5p8tEGt$vh}+9$50|E&%whgm?gJDsOM+tcrBt&%h>vA1E2?m|xT7gS69h9Uns}pa zwDSzLGuc9zF=YP<-J*Q`5r#Ul(-gcHh2t)V!`9fBDOW0bxtzghPR?P$!0|G}p+G#3r2^Fh&8l$rQwbc; zRjyQYK+a(F965&t1INn^hXM|VsQg(+Bz+JB~3AN%zD6^8nZz1Iz>9tQcS(0Q+Kq zc>p{i2ABuH*)hO80M3a4<^gbS3@{IXC&mEt05~rOm zVt{!7JShg42f)QKz&rphi2>fzCf%iRjCn}<9iWp!X08ftr<^k}G7+@X%SH=MI z0C;8$Fb{xd#Q^gFxGDyi2f)=az&rr1i2>#T@a!019stjY0pr7+ z1Iz>9U<@!1fI~6BJOKVz3@{IXUyA|e0Z@+t<^k}$7+@X%&yNA-0nms6<^eDp1Iz>9 zx)@*{056CE<^k}+7+@X%FNy)?0r27&;63fW^^!QoJS4q72ABsxGX|Ikz~LBR9sp4c zFb{wuF~B?kUK#_;17I!&m8wE0H38;3D3ph>!Y5vVR*q@_zD+=ymdzqu(`1 zu=|Zqh+Z$RM_}tS8{Qyi=OBl4>Ed=Q1BntYdW26rgU^}a%H_n1lk=ZWMig4v}kxz~44Gj#bst^7B8YW}6=w^qDI-g0#N zW1$&#Xzmi47Jl(UPlZl1M(gGWL1H&gP@wqR*v~tW>E;*6Hl61j>WlKwP>hH~pU2<$ zU3kiLQ(lubI_>H|X>q^e_ZI~{WI#igJxuc>;PpD_+2FTQL2#Df{fp8W?aVofxe490 zZ29N`jz73efllW@9y=h;jPsS)G%ikFLM}@J^M+xOuY;V|75lQ4qkY+-qaMxh(NeZA zJ94zdLupHomhu(YbC`arzHI?}%L_%w_R2vk7f$@A@LBe*8_X$u@QKhY5OD%DNN$VO z6ufF)2ST`RH!z+aw2HC|VYAgWJEIl{lTT;EWKH%K?6=>bMytsOO1-8gS?U`qE#{<% zcbBr^Vki+VmWW(5b-I#Bh`CJ2aaGtmG&3|sahym^d^+t&8}^k=`OK=)f>mDu6Xt4{ z(u>+Cne^3{(M2g{<<+y7xypT{U6+1Wm(oyaP<1I$jX~9AaDH6|sf*tB>i2a)J$mBP zvlGyvG=LUNZIs4wtAB`%C|P+_Yakq|ZzP(YYWL>RkLjlkSg@G5Gp=uGfi{#n+JNp1 zZ7BIRV2X=YJlk4<$zk>yOVwYyf>C-H_g%x;wTv3wefS#vJC}~WE+-RxLkx}IyQLO#UH4$5a5`sgU+nyRFa<2?se)rXemnkvYpG@V{u zgKs>$t^SZ}s%9P)#i(8FVpMyR7F#iDlZ*4Hh8o`oRj#SVTe4hJ?YA_!rbuImO)CKm zjWeI4)FOeUwuwYfH(d*yM>2CwQFr{*#-+VVr@)b~@?%HX_u@I0Tf@hiT~6;Dn^TPL zQ`D-TVF1*!jn^2KWsRe9?R}@l8}YhV$8xhP$Q6B;OspA0(l=c?Rz|UXgD~o3QdB;c z;~J0;R#$2eus6yDx6}KdpuWOIPWnnZdJr^plAp-w%SJynHy1r*ZYFxz+^*QDg1ynN%)x+Y`nsZ$xrJ!J++s9nZg<3e2I6}n-a$~^dD+0~K5T9_8ZkE)Ej2eE zEt8vxR>(<5D{u*wYu+a1rz<~&!9Cs*l>1_)i)iDx&l_oVX|3mZZ=wD zZXsGLHx-RpuqRq?Zf{gIw-}vdZg;f7+GA+qJ84&7B4bE&Ek|f8;5+j*dKUMbroH(RtA1G zW7omuGx&y$v#W`oTfbbkyc}RJ+4`EEHVw8L|}FUKh;-t@H5jrq}c zP~lp3^piN~yMCPnP4wb2`5PbPY_H6F?P^+3>8ZW1Rf5KMKkMB2!ateCr>ijEP;Hvu z6atLKaeMvsX4aCIJ`1`G3MGnhhY*L2}@V%27*ry@g(O_axn-Eut+>~&$Enu-jqH7ycZXTQUM z8Eb4>#!25Pc@{J*vj9FAo#_HfelXhQ0`_54`Dl*-mFP6*Ogd+WOZ7dnL}xhv_wje9 zkRC?z<*$uSbxC*PG zTb~(HA5pR&b|hmE@qeqMU5XVL;<>&1AkHD(Ajxf55V&!+#`u}z<-Ek$-kWY-NP}Ci zFl<vAVzKqB$xPZRQoq38Wre-k=nTvjjpS$w*`wM=f4YWIXQILz;fSX?i zqn#I4pF`;wG|b)&0W~68(eq8!M89g`o*k#&QB|nPA$atNBFj+#Z1VkXY#FF2$Su(# zrL|Y7gCi4U=3X(7)ZrAr^^E}7_?*IaNi}Z@F*hC+wTY;p@lig5%i*ML-)ylmwH=M0 zwjvt?&*l3fm?}2fDePSn|FPN93Tvfz@`cjBCTO+bp0%J;R(luVmNYd%EqfaYNvDvn zUZs8yl=gumZ>6;8BvsgT@na;Md`A`vv@INnq<(B$`;^?Nsb%^zHoZjm@I82$1zAzC zelkB%Mku+wh|}1tW^EBPOHd@28x@qK92IdUzwStGpr#^ie^q0gZLT$ z92ZD6)lj}p!F?TTwkK9Y#F_+ilp~FrAvapA%;jhaj;SI7H&9cMTcVI(rIlOVuH60j zP=w?+K`aG3i6wy$mJD4=Pb}m}WYuqrSOWwcv2dc0nv%N-9GvRjN|m zaU8W&f~aLxMS+SS6;XqrmK+ywErnWLj#}1k*N2X*B9(0^mf|^<`y4%cb|(dm%_*k( zn=G?OR(GcPywaR*X%tg^4QZ;s#ZQzcNCo5^S<{*63(9ncWl~I){d_IzQu?NTbpSTk z!w;=SM!5Z3ctuo0wYC%07lmq1t$A`-qxexQfzLkzI*YjjoJ38kl?uPaxC8Z{JhNl{fV}(6RErGhf$h z42a+^4;mCmIbDUh2`mf)Ho9@tUvbp-dp4G^oY@-<;SU;fbnR4A{<KWNy)Iq{)s$noFRRB${XN_YZv6L8U>l^T z)9zp+Kqgr%tp>r-oT>H&AH^Mx6XUY_2mASX7XPaFl|Bc<^J(Zdr`e&+fSjHF3@u4% zmr<-fk%nz3ru9E??s_dsz`>)v&?&fQt6Ay#c_c?dj}SV8&fqEsNY~E?T)l~r+`Ir^ zeJ9HkovphTyy`vtBy*D+ng=&TKL zZkq1t^0RY)+Re;T#>^Jb20-k6le$2b6S8c(*(xs`Y^J=6C@-CU0c;r!nJA^NqY{hI z7lTUmi>ODA*)#*JFseGA1^ zED9Zy}>oV}l0uJ)0h z+6G}%!$4DQ5O>}U!~+&8Oc`a$MISkDO5%Dn$*#29i1f3C;zs!Zj)p%cI=;?|Mrm&HA*_P5 zhRIUq4d#sCVvP}cxHbj#85&-!EoM``A73|SNuP#G@CuxX`_MxTj+N06YAv|U4P;A4 zcT*G-hI^;DFIcuAFg;P}{=J(oeu>IJ^^deoapIn1eY(t3Xe4GQZzPpZa4!k4c_P&^ zOpbkAE0v3>L|-N9o?cj7G_?F0)@JF^BD_Vs>D5dL-Ho@%tHXvHHc>CzUH0UpJpiG>gWjUs0FSP85`2qVv<>)K=nv0lyQ`n9fIxpy@oA7s6&~smq zb?azm(a^F*-oJX$@XpwOG&rzm(PdiOQo)f$LpLw7yc=hqNj>LY!Ix>z)8_?^;sGeX zjGyYWh+oHQ_;sy@8)-;8-3!wL)vJge?@!lJL@hQj{Pn#0)j-%xwUeo@HK^sKzCUjU zN&Om9!2+Gh9(NzPaz;YO*Iht3)jXM07Iw;YM;50vUk zW@vURzx8eWi1AS0=Qmgq24|}Mt$_wqI`&V(MfuUj+SWwfSPhS>+qG7=ZJl*fMV46g z6j839<_b=fTk9xyXOZkoEa^O zo&u^wGcFfq zbl{N7UWb^`zf#fE$^jSnH`RLM>;-DU z7ay|)^RYgjlK*c@vvGDtjr~SPV}IVbm5+n?(e{I&ziW~*pW z6Xk9=&*yAhx{Y;0r>Skw(^{a7v(He;-|i@xv6=~9!&uc{T#I`t9+Y2IEgqa-i#D{g z7L7Yy6BOS>cf*ODJ(}i2F~Hq(UUIE9*U~=Riuy;IWoM@wuJv;YGvjxuZ^|C~jJ~I; z=iK~qBlZ0&&7n;tU@yn>=ef$pOilm&T6sfi&~h@|zfb%uVo9thq(NojCh8wLN?)9{T3HA;X2Eo z&5NhuUe3Je>b>&VR$eSsyR3yDebl*Q3PO=#0X@RZR4B}t=&Y8UCpXn|5=2Q7ZA z^sck~1h3J;z1?Z+ZBy|HTI6f_$+to%BZuN3$4_w_IdUS0-RFoN0_dWef;QTnA+Zlm zCV2Wmkxtp{x`#c{>ccxxsY!-^4pl8|gV|mAG24pJ^g)$diBvTstvp6rMI=b8pd+o; za`mw(qpQ+8x{4fax(e>-iqfaiRn3m{V{}DHvxBY=E4`!Z&+!^v^T8_4b{t(5KaZ|l z3UG8)Trj%>m7lS4q<&PtmM%0IT+>fr9{Q zgOz)3OoN)@piXXCi#{iTkXMcuH;jfTrpOB0o-6N-Wq8ieAx>^Yd0#wLG1XZzmT4+_ zv+UJ-=lY1)JBAfG-%pXfp_5-uya=H(v!F{l--T*KJuHb=-Z%;i?4n>(BnqpDAz?b0 zLGaM_b2424&N|O@4bU~CYaOnI4Z-qPu9SMFH)7$R!P;U6|Anwsi}H&DnRl25TEz?` zk&suvV)V?7f)OUA5N;JkY+6yw{(rQhzN!*uSyU`A;uecUa4M2a7i%L96r2W{ys()S z^5E5%mq-hn%7?iBQTf19eM(KtM{M&lSOjFS7~mN!KKAMh7RY8XiP9Z(jqa~= zI2n7LiC#UABv_ThHQG-G#t=Co_6Gu_4~96quc@jb%=7P$O5}Y1fp%A=;#tQnrc$RK zH3WBHhHfEr82LR1I^iLp2)%0_H1ic|m|Te=jdBPYL%IQt`owbKG%Pwebur70$n))7 z$K;c9LHxBj&|&<-p&-taL|f|1-v_uD_rrKE912eR$z-7EWSYr9mE%h?8Q3}i$9GyL z1HDXw(yc6(E9~LO+<{>Vb^$D4e#w>33dOmGK-hx5<%2^ATe6ZgZWOf2uwrzrNLG$o zx(#5w!RR)mbiwfRkbPYiZ}zV=aHXtkL12kvALz` zwkqVmlI@VzpwT4T=nJ;Ea0R8hywaO)P=BEsiB#j#*Jle?V^>A_8eD?OGfXCSHgc-g zTV}$aP*mmpSShCiOvAp7`EazJa7H~_*6IMu5h~jsnXCb%^BS>_S3@_olb3K@-M3`x z8KWu7^~4~9fB2IDrho%Z9<|}Wm@DQ=>%oyMEksZWhil+uBTPBVUuRmB277=p*$5`e zOOd@(ZR}mBN0kk(EQ23zZ{E6?IFX&Fe)8bzN>6Rzx)z9|Lk3r-E~!pg*&m2}e2#Q9 zSH)K5C4f_~jU{;~9|2B^jh)@N@qCpkx3cmh(5c*qi_NFPwNpYItDjQgLqAla4iq>O z@>(|M+0xHJlB8kJ80)AK>%c@tJb}-)*o>>Z3O>B3HV)x9+zHK3o4mK(3(4NiN8`P$GIzP*<|^CGfXI+<4^ zS=kuu7CvKMZdkdtvg6e{0_Z)4`%(wh7UgJlicc8NFEF@pK3bu^HS8iXYvjm1JtN_H zVgEcGFg6Z3U<8Vp;xO*@U`9ALrL=@Q&ngF03$L^zT41L0YILv>cCAAUb|p6vTY6lI z8`4r#eg-&g6WFqjWW1UoDLv*D{>>y=<-_Wn{hZX2UCE}SWQFH2EQ@Gee8L77jzs&i z#IirR{K)c|G-q*>GSkFjr1mMAL0V2uBdX<;2^q-a4tQ3L6B<$Zw&s#{;dzD2B#kYa zPLft_QcxUlJ+qrduhj2ft)MQ}#wb~M6u9@35}DFuXfYo%Iq2&NB_uKmdD~ z$%nP}s?w?kHb4~BKy(7KsB85rQ2g;(If;`##@N*z}7tpKzT<(~HhIBL{rv6|-?ouv@uMLz^TeqauF@SxC(+lzHWlUK^EE{~gE(q|NBj%zR1%4QwtNgHHbFsP9jDDeXA};c}7!wU|E^gu1B{NQ< zV>1DCBX+bHSa}YJv}@Q|>?svN>mQ)_!fclY?!>@Tp(IY^(LB(K<87P9+o3kz#*Am= zc0?Gq7{SGB#;Fg$J3qZl<_)Wx9wph7+8sd(X`ki&xis-l8rnnIVQGX&gv|w130N7 zp<}S?ESWfD48OBfC71#w;nJdLE7HnGczcRB*1enxX;jESbe8azT~|p2&gw61#!bE+ z2i6UGWpF&hvbb(=3ua)oN2Ke@uzM)!2F1dNS{B6lHb~KTH^>7EAGKc4%Uu%e$>NRf zs$MXg!7)VY1-B$Ocu+5h7qY+_QF0_R#SV{A8~}oVq|;N?v`3UZUVbipj!sV&fy{dr zfsgUzS3F<8YtjzQCa|TZHbDZ{ItX4qmral!@gkdWH*LauFp%2geS~epu!RiEMT@*Bi>QHG zxA+#Zp6Rj#uKQ z!I*`kLDuw34o3leWMu-LbHpM?jQ# zt&%ayC|gs|UWT%8xiu-3e00rQU$q{eJ_g;@uI%>3HH6-hLznB z;5x580cx#08w`b3?V)@aUb9yIHz=EWD~4f1EmR)v;fL${9`xQK%a21w`w$HG!nIHb zq2gWBa3EwHeTVplFiZHm1~(AimoSc`VuC?Le2<8b6g!9F85PB_sKsDLUmUgC1L-V| zum_m2n6YN67Dw$KhV$WzBfRO6#NqgLag+n5oW~_V&F+D;xUqz)U;=WE7~oY9A6*5- zz7bxkKyzRMs~?IrtsmlTy!4c-A4(?TMfLN`n)+GH_(~q_2hi2eZKxk?i$*>Suc@Eo z5vclMn5&;sNIMa49>>XB2rAUO>Np=Y7?puRbzDIB0D~_id?4YFSF8+u1|poWQe+Vc zJJFUf@1P`xIf$6~Q3XksUIY@BTlhVoaaDsJKZ`9N$l@eUeyI zoh9H5Hws^>qgvoVBf_v=I>Z>B4)L)@hxShD)^vzfpXv}Pnhrhhbch9tc##g_B!Q|n z-pTl;Lk9zNI&^s^2=-Av46o6lLl7t(Vpyz0`vU3d&>;w_)}cdziFN4v21|zyBOLMy z9eS0Th%?vl6z+#Qz{Cd*X}X`9we%9Hi-M>wm?9EMi!4n8N|X&HRa;X2p_J!DRKw^~ zxSf3>ruvvLRUW2El_$v{)tcITT*#>2*;$CXB2$f^beG3`Eg>+RWcU9g^NziwHRy=_ z=~fkqpVu33GWQzDPC;?niYBXWF$bY?&|;`+YBAEeJU&)?T0A6W46nuTG42*IH)MOr zu#*(_kZG;WP}S2mL%g+_U%NKLT8Vg3oB3@`n>n2Et<7`+RGYCgN^B1q!@V}ceVpUe zU>Fd0iuoucnS4Z~ibg;-fB{~K_*i2DzN_*uft81Bq~#&r%JY^hPXtWFi^_wOBe=f= zM_OdNmjaoNg~!*K+D@JoSAK9+EgL9ASPEc&1{Yv#Lon(q%* zEMHmd^iq=*X>O6yJeD*kBG-P)vbPAN^aRxqm5l7&#_ySodefy`_Rfa76-FS(&dcKB zbM1aT&{*-bIC>F`&reAt8%9WL{gl>;h!(B@Gnw#&eh8{;19ir`tN)WIvlXfTlMyi! zo}%lixYkOI^DubxJy>iq^XXETmY8DMXNV_^=-7CN&sJ@mv&gJZ_datL4sNhz#n0ea zI_yq3B>_EnOG6UNmL3N;IMFY$=f!wE&~#T7GDwni$3H{g>w|9awWNlxkX*rmNf@0d z8iOaQ0W?ok_z+bwK7>7`G-ydkM_T4h9%j^4s5ex4`W_m`WLO z|FzSNBXK$I2=o&;cA~H=8HNi5}{;etVK)}kwm@YUiPP_PH1nH3c8NE+wWeiH! zLwj5a^$ql`tLPc+kW!l8i}v&{pk1!vYmMKxT%04boI`0pVv>ot4~z=6|=~uB!poL!)hr7#r=F@nacl@g7?v6 zQu+NiwP>F#@Jn(CN@8Lu{TeQ5RrTJ0Fjt3chpHW~H5r+>8gQORQaM4;-bZERqw`sr z`G`hME~BiQr%#V0%*c^sbkC9GqdAiFi;sXV?w{CC^CM|rQ$JGFHD832owQa%Wr}#g z=AMfP(~bhZgfOid;7bX!)d9YoFik7qD+$x80{$^!8dUkC4WRDNJ6(qWXYg+TDF3=4 zz#4Z@7QYOyvG>10VPx->w)1_+GJ1eyLc87wIHJoKBkm>QI9yO+_v2>P{K-UqhPBu| z*$-GZo;hZUShob5Id+*b%aED1_heT_L+HE`O|Ek_K+&kepvldQLk|559=!J{1D-&`02;t22Eaa+7_{A)wEn6n5*X8Jr#EZxEkZYQOP4y&W1ez`|{k`MW^wR~DsW3(sx zPu796i+wk}RPx4^%-mWHmFCXi0Gtrpid$?OSyYRZYgaT~a=lGrExWnYC8(yQQm`h& z<8hhTF?o_P9unt)J=uT61)10!F)lw<_tgr-lAu5=S7ih)_JW*qaMGgf4a1+ZMO|NR zQ8}s2=r}BHd+nc+KB%I`)QC=WKs{+h%O&i~a8eDWxbfoTK&##8A}Koawwme7$f~BD zdu6b5yTtMjK_O*GPg_pZ1fz7hIV}daKuTG81WPlM|p@}62W;8)FEN(dJa(?K8okww(4J5w-+9SsdP3)q62h>kWc*-8On2Snu%#K1#u( zS5oj*f+}ezeB^Azi5jeV{aR?!jq}9epY|P4SH@GJcMd!)1y4`GGg1)yB;&Ye0R$7> z4|a~kNu$wuNl0b>1~5*|;31YG!w_}zc~KGXVS}mPkey8U3~teDgzcL?uczP}1Vc%8 zWyhR4Mtsry7YI`6C26Cw8|D&Xer=``LLvXz+Qr$TOU&@VM z6uFd{|47+bTiLEG* z%Q&WJ$V{qLEZh~6+_BOxwOA~_NCaxJ=p$)&ml$B^iiW zF%Cr%lc|{xkfNUIa~aJR$6OHv$Zmx+S%vsaMV!Cq%1t|59$#Y(%d7BoW30B~kMs;)SL+nN4DZiJ+8*vv>6fvOBubq) z<`dr~A<@vTzXu$h?jhbG;tUV*XClt@5bqLk7A{bc4{$T5srBH*kcMvq%h^}nHTz1S zui01P)1g-EJCUgI*OSG8q*V?R!{Wv-IG%kKC*JJqKN2ERk%)8J#u3aUJTp#dng|%v z1&fFp*ccSYB7{s?>j=iPuPR|O`5==X$E| ziw|ede%V~c-$BzI#rSWuR2zl zwOw|5aVI40kuq4}IAC>QF1A&j;+;W8WApzp9$N3TnFBLm25yzVIxinlL^9@hc``CO;{Xi-no|qy&cY zX7WoPr5vfxh9@`^b7L`^o)Wpt#MccnDb@Vc=3JmJ+|J+Zs#P*o->$QwO*YEaA&4E_q*ymXy;gVmzm*up_|E zT6mXN4=jMkd#{Lmy*RHoC;LKHxtRW-kNyPlVa=1O5wP_W6MSN|@a} z;P(i#_5r_7n5IiU%h(#9_GHJvmF|2H+np_5c$AcbVd2p!cuWfZAO(+2!Q%iz z`QzT|>@6Q=VNUtDr>j#U;br>SPw(l<&|4Q& z36HHP(_Yqn*lFciEhT)t0PMKYPrj|8bZ)#8kE-Tb5=c7#newIfnZ z*%5p%LvzK%K|1-pm*f0TqPMB2;_zS{R6xjtF-|g)gn}H7p)g!r`_<{3G5!yWv zh;&;SzTt&a+HvZBZt^H~KZLfdya)N!*UyBcH}G;JF8Wl>O#nJJ$>ig_ zTktsxW)-tt54PcBf$gW&S{R5{-Nk#)a2#dt^Ic!~3CKlx62JjSxJAPW<7**?;uFvR zE=D;bLc2<|ceCx?jN}jwz7b-{((u9>w1N06#Xx>=LITDoU@VqoBPrt>uA7mBeU_rX zSnjhF8?B5I;fLXJa4YKMZu}PjPr~M7ZoB*%b#N52NVUj6_6ozWRS*cpT^Y9}g|C6Z zG$!cqednXluYDi-7L?YMnGYu*?%1XRUZc$hW5?LcX-3#g*J=pE7Zh+UY{zhX9RYCs ztzM=KKZlgilmveO7!85@&&n8$a8EobG-n3E;u0FeH<~yHZ9a!cbpq%Hp#s3 z>UfB^(ZiZEES{~!K|66EUGV~w%|{*)p+RGZ+R-jtM%x2a6DHL{`)5>QXf`oetV)-( zngNA*D1AEcFMNZ-DB)G5ID>zqKzxr$x$zU{HpvABVz#A(5giExvhbvQ#ng<Y1 zt^6=4Yg#GV!eb@!x2;Tu7@Z3`1t45dwAIF7B--j>>KhoLs&8m0J&oooACzI7rtH8y z9ISWi1P)BWYzj7{;Gh(2O2I~eY{YKeY$~X_4+pGC(91`5AOfH9YTw#v+;sq!)&`4J zoLP!#j27BeoYo=Bcq}9Y*|E2Ay{z5$hscGNfQI0MF_YmwPryrs_`Qfvbzz=tVCk1^ zmN-F4xg_+HvH$dHyfIRh`%va??`!H6jF?xJ)FLjKaNz~~JvG}_G3Xh_A7f(49jgM* zN54!~FTmXiZx}M*#y&b@9bi>*v-(>JK`s`h;<&?mPmEI>?3PveP`OL`MNn;8s#SR% zNPCNsGN6pDf^aeoV9RQLv@T&BYlB<7)FDB!?j4$YXWRxE#WwbulL+No*sWDY9L5Hn zNZEO~%-4a;*7)NDeq}GjcW+tdqaxYBK_|FrfMJ?!)P^7@&Zo?}SGzux19OLJV!}n4 zYoi)zc%CC4tHz+*4}W;lhRk>P;;{l&VaSu)TG}a7=bQaR}aH7m_^&0@`TK#O&MXRSz*cc2L5d?U%4g*L= z!F)OPuLW#NoZ-W=3_i+iAE7F?;qTj}?T2ILyg84{Ex5=iFd+=xT-=bW-fKS@qK~Jn zHwHcv&cQVr2c``~>nZBEr41RHk4QuHB1S`|;b);pP%l6WQ+ZMvz>F|so*WH_@9V7>#|R;>2k(vQ;t^VOLU!X&S|ly-AO@DpI2ynx``G=6Wk!{yDlb9x!kLg< zmX1vMI{0#dVBYnVqd|yFmD$Zsk20D3j5!6|%8|fb1J6u0Fnju+kgt5d4>y z*edgzNP)Pzg{;|hm7X?wu~o4}u$mW>G=VcuKt4%E^T4TG!{|FC8S*+@8{ugUaGH&w zR5_Iap+9w+NK3P(iNG{rN;1x~f$PJX^}HKTCt_(*f71hc5PihdhE5z9-Beu7iF6cV zbTnuYzBtU`R%R+}yKjniKoYRZn^x4Zm@HT?1>2c^J9<&L)uMpWHw3klp>>lsNr4NC z;YCF|GpuWSUaitsM~2b*>GPI&M)TC$^FovQkn!ac!#P2{NUC7R1j#OY9QUxlJ54JFUDay2q>((2oCkbUK3 zEzz?V>ahlQ`8q~1mGRn1R%cSA?56mGU|Lefh2NqkJ~S}Jr}53x<~ZI{NKe}|pWN;= zbbRp8VH-zNP?bUPkXUP3v-(oy!Atm=k?(@pOvo#Z5oeY+W;WvcUggvQ6#JR?BwX9C+f-XNMIr-|z&+$G^& z++`XwB}~|x%kPoPSl7}LEMW;2TUdh4gp6I-pJ7Y-4r6hm{SdbDDR1l5-pahHdR7to z(ibUbtvbdU%GRA~D4Vcpf@&5iD?dgiR*1eejKlTSX+L zqKC{$ZO_F{nv#n>s&a~AQ!K7Op)0XjYlcZdr#rtSx$T^Yrk8o4ad{y>yEGiVGrHGqL~c+5+*PrHsne3J9{ zm>&^QEL4Y>%R?e%dOKy-jKz#etBCa5@(ZSpucFGsXQpGvBKcFZ*T#KrM4I_1>~qp5 z*W?kE$(Z9T4a?6L2hjlx4SAK3!Lbf8C>3@4lf~7#U|_8^EpmBF=@cWEw*uH_WSfZR zv72-RC=U~^Ro+C9Yin(3or&)$29;f<#Ej!KS6Qa~rt)&(6ho@bmFtzB<-ndiE6?q%ev=m{f!gGfMVh>@9-G+b zqQOcxmymqLqrP^lo_5uP)33JnOiMfF;7r%-ny3Yax;!58K#jAAb%GrOX&MOKcz+{A zBUwE ziWvVW20NJY4478XOWb}JW_oS@;mDSeQ-)PWAA$D|o=Kg2FHLkHxG9|r#)8U@h~{XR znP-^B5|wKaWS_71$QUc8J&LNaMO%&kTzRTrvRV=F7(-!5CqG}6Bi0O4T{6zAwT)^S zHERpZWmUHBM&Z7XiyK25SK}cd-;2ix&=E0yCA6<0Gy5DfA5AJf*{@^F)p;j0c4FJ; zh*4Tb7tX;|mcntz}Yg@Txou5op`0EvqUDJHg8NsNO2gOiJj6x=dpBsAeeGTdAv)_Nm#GC)4~W z5N8aNA(B7zltb({3iiO^mp6iHHfmc!=|rnsHTI3cf=w_t$RL;}Fgw}{IiV_*;SjVs z1+LXF{)lr&Y=wETG{96w;~kA}a7YxAE{yi}2yjZEAm?`QS0n8}$T3oHIiPQ^yayTl&UN-9^8sBDHyfCU5ce;h&z{3Jf z%tf6k-ce+rKz>SIB3lRKDFQvLjfdzUOU0--y98G_SUzb^?qx#73OV<(&}RDPN0vf_ zWT;nbS#3%IMHa>^^&jSJD%sYTA>fnw?|7_>~&GDO8du%VxH%k!e{YI*jovyL^jQh;aE5dOh!LnLIyFZ-ah_t?uH3v#@U8 zM@*Y|n$o{p4Qn{LW%88ifh!N&WaS-{-#0RHvIZX&3kBwvv>_Gx>}|!CBG0B)wP9Y= z`aL)$j8iq`H5P$fl2c4RBujErTBhhlCxuXJoO6+nHUyE(i0X+?r(QTJ4uSD(xnOHW z6mN*{KD*aRO?~5@il!PT0P4z!HX~d99{V%Lnsl$DrDG*Cy2&v1MzcmQ%vJ)3Y<@JJ_X&6YK1HNVTfdQuFc+tzAX3kN%I zLSAX0yyhdnP0mM4yC$l=Fh_yDDH`zFEo;yE+Mhueha!VUI>SDyno~dDbz0Wxcs0lM z0Q+w9mhF=cXrIY_065K&#q^du_-IQW+;jJJr0Gi54s5++He#gf9h%M8UYYIS{PEkG zEN{dM;Ef1#VF2(v!kpRHItadN%yp0jxOE>6WV*tEZ7<2WL0s+VLa42FBZF+ViGNKh>7q7Jcc+mFkR#z#w~J?uD{6fW5ml;z6s6DB?})XA`Z;qNxa#7 z*2x6TexE{6dDyili;aBoCO~C_>Ni)PuNlTKg`oXnQYb>@-E=lA357 z`bDQBRVL)Mzg3ho7{igk5oJ|5-3<>VI+GD{(OJB)_GU+C6AW1{YbB+&;ZSo&1N*wH z6Vtj9M#p0AaAIwAZ2EQ%Y2Y9wk)ra|F2zU-EkY#aG(yzo_<(F`aSSt;_6X;(f-}M{ z-&2KP5z6E;OeVRl%lc;n9PJkh;xrl zJTK&-QjV+mVCt3OsPPaOer#E+;AC*8vQ9OVQ!s;4T^sA&=|*kzSi}8%uLCi7`Dq!k zwXWViNE)4o++#(Qux_x(QeHaJ^9wlexhXoI@w(3C)d+PVUUX!NRahv5MWFI|OGy@D zErXfXE+t(+Qt?vKh3-L1Nf+@U6DmSTK5QxJVxmp0FCl2To`hUA-vMVtR6@lI8Kvx| ze5B8=queE->`^LT84RZe3t`Z z`NU%O>LaqMZ_X-D63F7o)*`|2~P%IEfiHva<*XyCp-bx4E+nslYLT8 z{Zs(kPOn)fC#!SfoHuaIFs@`-ta)`M>x8-mR*skVwF+$7KcnKW$SCkF$u5+pl;E2R9wS6 zxY~W$1sB{s+b=s^7fs%wXqU}hv}D3Q`u%hE!ugA%Yx_+`=ceCe*Tu<@;rp{4#Xt6) zZG4RA4s#YQTo7IVa?PSG=f zrm*7ul)tw9l$|kSD;NyY|BTC(vr_r-h-JR zHyS-wVHgii=u-yM;UM%O1Fbsf)Do9%cc0nr>SxO+Nw(}L<7`csw5>t#CN{)sr7f4q z;IrvZZ!ocKWJG0Y8EL9!rR1ob3rsXs&pf^L1}5ZK@M1V=(UO)cMbegqlq2Cn@X2Sh zfrV|b7su6_3+uBYc1A@+H-V41rB=tvTXiYcPb`+qr7XK=$+azq&kJCo(ijJ)wm#Xo zv%nn&EJ1bS2;lTs;OJ({g15yw85Pv{us~m!jBc^uY9*{(a4h@`90Z(=ZuPZ%l5_}ovt{&NSIcJ=HxKE}{9R=}Xcv$0CX79RbS z(N#8ALPo?PaX@2m=$TK;!e+275oZM!CksJJ=V(wzm`gy*CZq}7W^ z(rih2c|^oXDv~lzjzH&fDz2H38N|y4f=$?x!C_#+mfRv7X@vARe2dLYiwh((xo1ceSxN1G}5KIxCVofu4s^` zL!mdLo8{IcFw&);U(>#lj>F-x`Xw@~C5OVuT&Scwad^yg3t!VjHQO3*eqe;k>BplS zL;mlU!{awDLdw@f6oD0-GS0ekPh)YCmde2<_YUHi*sJf<8}z zpn>d;9b=kj)RIk*={gViWKl*#(e_B=o%V?RASUQFPd}o1o*t(qtz3HhFFZ53U9-Tr zSq{ngOoM4n5MV^jiDPd*#4+OLDX`09*7deN!_<1KgqeCvEPS*j7M*LrHI}8xx`eH< zaN#0dW6@%Q)>w9f-^Mi-)=9(*;A07MF#_=Mgt@SwHI{u48*424;?{j1km5&(U!dv> zC9P>IRD#FQWAw+Xj>li7$vTT-TI7|9lG2MF@pryhp6D5tKNmeKG3cR-aN%*ewyKGI z^iw2VhN*joaN3aZY?|tIGcCM-#ww|*C(KdsFrEul&z{I00Dpc{LaSj*vb0sjDq?ZEMs1hJ zf7}X3RJTIV=!>_;md1|D@BvXard;$3anP5>PQ}|@7_;-mj+0GgrFtiR{3br!6jV3yHnz#)ii{rIhG4Rw{Mk^cs^` z-l{Eb`xG!IjxobJWo5rcYb(RqoSxwm29P|nMQ<}K(=tq!vzI8LXqZ5q^%H!HF*lj( z`I+FF%(y2rT-pmD6PiV@v*Ba9p1&UAbSJjl`Wl-0w6BpBRbR8j^)+f=yr@s9Sf6q* z`;=cYt7K9BilGAvdqPx8aNmpf3FL^s1kmA~X^JH2Qk?b)T8@BAug@dfhm3XBz%nDo z2J;FchtK0O<`k~nO~gT$FXM@Z{K18n@sz7yxyRXfgu5As@)5449|~X755-_E4kal# zXmKbX;hcG)(Ld#GC)`-v=#yG3lSH2;SQqUDV67-dGBSC; z&6VTQ1VNettD?<=W-aDAQBDlj@V_hghnDbrVZZN( z%JyxLNg3GQzS6zKKK{L=^C1X&azDuO!w6hlSAK*y&0c?nnB!eLnoNK#Q2?7}iut)y zkb>Rw7glbJt9(kk;c^#N?%u_z0Trf-Y&EqcqodL$%oqzD1MaqPet#Owy+(6TH~cvg zI1i{PsGqTB@;CsG)5kedI7^A4Ey-gY9yR%y3LLcfT{WCa6QILHooB*k#Ht$@W^pT& z!QkSB@zVi%vJXlX{>CXp#vK?o;F{^lUWWSYd=$08?GHt+VicPOhJz58UHT5HSzl>q zefMNfX4JnzeJ6G^qyWmr!hyJS{td{^_i!oy9T$E;i{JE`3I1?q4s}xO>0Llm5*oBX z-?$P>P1*APU^80=wI*qWzEhW&3BK=L7hH-o$H>MH0efhKnPR56VI~}kr@|BPOv{g< z96ZwJaojuR=tv(%#gRV4c*EH^E8(sqeekATF<6GPU>Jh)e0b{_{vx+a@H`(T1ED8C zC|EXP2;XxSB@t*WJSlv8HM;N=v6J0iG<;8`5{%$pw5(TiaXEgVaa`CD*UXU zaxf~&(asyn(Tcmu0iVdq!4Q;#H?JH_R#Og95`n66r0D+3F}3p=Hvi!unwDvKrWN&; z)puo%ReAGB*p$uBV8yLMUuhH&xQ)iGG?62z_#?SNcFLe)kj+nRC^nTI$>5!RES$23 z9EqrfTOlM2D(vAQN{nM7wB^AKGw-frJQwy;gkeTciEsu~Rwrw$wNh$V$L_WEFce*iy@3m$k4VynJ(VF zHp;{xCtKq=18LcM=^sSQohm#Sou$o3Q&RbkNkL-c&EBf#|I`%A%MEP8yHu=wPOdI| z+$LS{?FdeJlaQTw5~16CSTC7Za;H>NMy7n_hsqO_8aOyk3HK`WjJ_H?M@}=JPHS#O zMNvqrvbnW6u1cP!W>wi-QWc=FcWj_YmZF<-XhTO>_v={5Cz?m_?5o33~~Ju`k;-bQzk*s!3zv)2mVI^WPd~y z&raI{xa~ZAbw5=f+=ZCO$!@QT{9QiZ8EY0CjjtqOQp6s=BWd4BAiQNn-!1pLdlK|o1He9(0lK1i-5vBYN zE-UL5HP}-OLs@1ooZeQka3`&1aag4SF;vb(jk`Bhz@p<+jI}G5W$T)uhX2FLCBe$= zNRtVPDf2xNd-uz0K&W@(wxJw66TG+>Tz!pwj0unmFo2LUx z8SB8BOrHsPi#iCmM-Ig&(}AhPhvW;#Tj5%H4#F+}9Qru1qkdAZII!?T))@{iL{A3- zjt-+Pn>HlV7_9{e?2tm`cd8g-`mY%C55=bnR^?Zv=-99F>*d!d_@D7Vt)^b`kUNjH z#MXXyu3_p7^E^zPNW*$Vy{rS)P%q{!)fXFz+muK&C_QH#@N|S1o{BrJ#5=YFFPRv% zvMz6^m1(%ER^V!*R&G|W#X!^wZ(gUxl(A!pQ?u|?`Hw`U{L-F4uPJSX9zN-P72)SG zLp<|K?)p5PWhsUls~M`pQI^Kg*o2`WeKS-Sn_gxTvj3yQqzAenQ%0rz%P77WC|>Bq zb*$wc$oa^sAesZkdx$*K(Ug~%=l1fMr&H8@gzG@VuUSB){nS2sOp#3G^cY&b?}Ay< zoa@nj?9SC;h}%N~LgR5w7r z;V)oW>rY$AIl89-c0Pj(W{U3PP;o;e@S3cUHLiO$1IwBQ{B?J zh^t(kT3iO z_@MHAu#qdgmcT}`C;KZ%r}K57b1xR&U}$o-B{?ZC11j1HP>@+#LiXj~5-}f$;_OVO zi$LK`q82Dl$f$+4fWRR*07oqS9pK?(a)saH4zt@uY|rqsIB8?4|A2sw+n2T3V^0nz zoQy6PVOoRN9cnmHmiya?jQ7;Al**~mBr^r(%6~+tz`TLXJCD5I!5zKpqmWBVW~AQZ z!;0z+th(h8OOs%IQCU>c#b9V5%2eE@7Y)4ajDVCYd&Xfhy@?<#EF7UIdoGIm^9Hb~C7mXd$`B!XB9O3F8yJlHB{tS3CR#9L&|K#NJXUaz!;=~|D z&E%!2(avIr*Ri89?&gW0jX~irOjE=e0_{={eTk0etmL9ah*{#qAEzU}g@@vIp*%C$ zEJHqGl*lNHJKqI~T;Z>{M!Nu!33*NUb&HszQC@3zOk@=D$@RMFHzF?CiiTj4B; zkZY&m>!jc`fQ9#1_-)Dg$gyMG(W`gQ?)*E^UnjcCE-~U*tVDFakMQBcb<2c|SNMP_ zwquIU4+(z55)-p@HZGo#zMn?=ABZ%un<0fCL6*_>7N57JbE@M%!1y02K0^vWM*O84 zRmXXdaXwZYh7_)1oO#u89%7t-DGoyl=wOS5Ynb^r@yPi<-pijLM)Yk!rH5I=nJ&VG zPl?^t#p8YH#<+N;dL%6sL#$N0xdgnI|4kCR11heKya<#(BWe#t3MH|S59R*=jjX%) z&@=pX+A51b1!k$^5iZ*^IhXB4##yU&QD=)_ez=iO$N%RbIcrU577ZOs)RX(%H*`V` zCkl;bPlT6X^r+zK8qA;?hYT$DfSfu~bPcs=cGpWf--Q(znKY*5G=3RoKc6nURS<&(Pf!X>HCS# z+Vi@2#771sXY#R3?8142@93;zk>ui@6KBuU2)2XR8eM0SD13;bX9^FT^D7r9S>)t&9!mD z?@<6J&w|UUsY;Kz%GlE}SUZL;1D1j}MV7eHFVt2n0=da(O9KjMPr-QuwtHtlo7z_J zjqKdBvf8VV2|gM4>T_A)F=0d+-o6DsYWjLI*9N(7;pD_GwS5b8T+@9E%+k(XeiT-T zd*oQAh!^c!cnVl}+fU=xy*H4lw;eCce4Qn#S@Y%`!Z@s`oT9wHgLK~c$($T)#(xX` zbNDZgEI}ZFzOzxf4Wp@2t`3)_xaf^bypjDqP{@pLdS$};(PDzRXo)b05k&g|EJcbP z?hCl^8;+#=7(|f|45AL?4TGrf;;uo|b>Ja*qZ1Tx_7{*CMDgYgqLeZoL><8NI4%#j zXn(>u<&C#c4_#%z1I1LRv9< z{LaSpV(%pg2ImJM5c70hynE9nObcq7<1bYZvvM!;#L;)aYo{p=}&LCtXYMgDXRVZKqve;ItY$`F&faQfP_^O=Ld}@spq)F#;p$5t znuFCB&M;5HA+F^6HL9nUc^Wb8?=)a!l22o`1~eubz_&G>24D%OFAZpP^HWywY8X`- z*VNi%`OVFYO7d%}&aWxXZy7%7CUtA-JF_Osj6MVvA0JvPh4^3^=8>B@dm^E!jkPrU z^T;Vao=4VvfUOz)1#R#H{HOCN-r&0vxwygi!2mhl*FwhFaeX|qeG;lH2`t=*668;*N1r+|H+Ie? zWz6X}98doLyQLXo_?& zR9K%Al1L;&d_brUb~}JO4b@KZ4TXB5C;M~mo1rdu#{U}77k@1T`@gu>;pujw0@{D_ z1>&DB9q0yged)kaz?%*%cdn4h(ajyp8|wK;+||w9BptwtG33fXbaT9UI-rzsH+Kxv zXF}ehqY1Z1gehY_E?XU5Ui1TE+$bE(>3f_Lplw0G zcRQnVEEO3qf0wn@ip$dD@hE%28tuuxMSC$2ZE9jSL&{ltORC!pVn^o!2QldulHh{H z(Rsv4z$S=0QQAyMtUR9r9S`3FU-j(RT=;9{x#)7FD7{K{eQGtblZ#>>LFzo3VubTF zeO`1v<+t<%Q1Hs~JC=o?mYmqlkm%A{Az!vUAzy(ccwf)o=@7zk2DoB#D30HnmFF1D=okJVo3qv2DR2udzT* z0;tW9F2|G8pi||J2f4h7@tD-q%^{rrdoL@&CELF@+54`A&F!>zNFH>1?_1328x z`T-^H{2OwKu0b?ZC2x=?2C&R36JCvLNV|&zt*~g24=Sf6N>RBHVCNvH%HbB#`jT@c zZ6-8wJ=yozmh5x3qIU*jD3hwoxQ$$PwEGB&PpqrP26Gna^t$LAW@D!RTIQ9HuEVX= z1ZM{2P(F;JEcDG*eF5wr4E2H?cs61tp8>m`!V+722aTHbw4%q-k1#E@Gv@Pa7 zTkuI(bcV9Fz;>mBLbXi{{$dZn|l2(RPQ;;hv7AP%}qV>Zy3gEz8UwB zJ)C`A?iM@(%Uj_{T5*R^wDS;6V!?J8OUVAShz)p(Whh3PZ)tBeVo4S*(M5>iz>8Dx zk`(-53SO9k7XU==sm_b8!zMa^8}qa?&u#};x(wRPata@kqvY$pci^FNKN=`r@duc8 z7dEVLEV8p#p)Hn3t7Sg;G3;#jE7O-P3ur7!JKHRPL1 z3pBN=5+xZbd25}iZKLH;c}n~y)lti|G?AHjn#ji*n|p4eAEt*?pHvUYl-b;WIXz^4 z5iioi&ujGXr;KkV_AY?Z!+}^@4KNeR@jb(9^za@8N)H)^7JD~tW*aY8jq^DqSn1^- zr$IICzw!^--A4AyRj#s853d9@`Ny8`+^?&A5Aw$;V&prtPr zL8a~7xbqK6*nr9NhZ77`PO0f!dmxt9g*MBWfD7mAps476X6xk}JzxmoC8Y2Q6a&j6 z3|Q#A^h4$>T9669G|v1*(St};Sr<%ZLV}Au^^x2v3z9IM6e1_wjk28>RVlq4oU~R} z(7{1p;~X3%{ZU%&KL+R=90gsK23;KonS*mLr4#aswgPwF92`A+4vvUiBO(m)9GoPi z#=*Hyba_o2oI8oaE48?(;Sx||V}RG#_-KtS_HWKl+JLn+Hj%Wgk&d;sFI-zAQxPv} zYXb*W+x&+Z-`d(E0M*v&gQt?V#_*c9c6Hp=7>2g?C~nr)Zjw;=91`r^*3zJww$^zP z2T}Rxb|k|9RPk;G9NmFSzpd&qgu+p%v+_04NI_EOE zUevaRux%xic^EtV0wKPG^)v}sV|$iC*?8>GjwW@X@@ABp_V!`yV<~y(P~;LlhiGVe zyg{NEJ20zE_)A=^Dc&hD^4VhbWQQT08xjy5-i8=TTXo@(K*6_KaOxiU*u*{hDYG%h z{dwkacp<6Y5xtQK=j+4V6PV7xdA%~B>Lrz%?NRs9<7 zo5lj<5u0~QA8QAIipoT(uxrvlbk86OZmedhE&-`713cB`V|AZ!QKCDhx74mwZ>2XH z2edf7B|8x>(%U@s)*A=>8UapCUjyj;uP0HbcW8Zu;Waw?C~8#xm(q5pPxJEDI zf_*6|CcF<~u+hOQjOGX3p9HK+V%`#`z;7a2<*dEwT@g&3tICbA{`T%i<@f3c#N541Li6r;}c>?4Dlnv0AS#mR?aLrdL#lRIel|>D5T5S7amNMS3-sdNs9k zJZ{}@0h!vy8AAtD4kRy4@UBxd=1~R(I6XbM1o=YbzeggxO|I+Py8cnucXa&|u7$Hu zJG8`q#)H}xH`Oy;_#mWFbRiL^rm}bsc@lWhd65x6woAGcppQWKF^NpUMy#^awrOwelX;HM7Gxhd8Pu(p}NoqYgiulgcB85F84Gl z6S92nX>>LuFRGR+>?OnJk;f=^#8x&WcP=jSAbS>f;)txypiT2&0qn_+n>q}t=6wRq ziR~eXE$hDek?=3S-6rw|kBPVGd;mTgl$n>5@4mDbn+j7ojGci?ww!=EEfE`gDvQqd zO3?cQu31B|DLC#&WXs9P!Eu2cDvhO5ai`FYAfMIg;)lJ76h%9XnjM@ni@bTY6{WtGa?VkW6^ zuX;1#2XWmR>-K%mi?Q{|KA_#uTnq+HKGmy>a6)`=O>-vv3sRT&z^rpHc~&NT7f<=} zV2*9g2dN8=h6&ftDA=`p1}AN(6CPb-B^m|lmCY`sc~XO8tg7m726>-t?yjh$3}*@x z(e@^x4XzQl!NR&27f<4PetiJ-v|r^m<`PbO_hf~H0nv+;*xG15w z0l;4L+ErkaOC~6Riyy)sb#4wWo3e!ltbj$+2GdqV(|H>nt;1Uvajik&W-I{ML!l8- zwf~AVE+vhvJR0j6jgrxrAsWPi#x0`32cwZVm7vpx4ZD~J2`;0JY{fRuk5HGH@MByH zheE;)ned;&{0mp-o_`|5r$ihG4W+^SHy#p$`57J(gUOJ@U`~yzCUWNSbA~$e*hAQv z$1ecm%n7i(!sc-RBB$n&#ZY6Ad1P_i6U-wE(4y007kVR^C%(t z<}u;lGmmP7Xkh;VwYY)(3|$!$+tq7Qg$_uU4FKJqbxW=Rw5?UA{VZ70!S1J2|BXUO zCaN_ZW>!llo^D&*-nx7_^>ABH55ESXK$q9&ZFn?`w=UwKhqp=(`B1IPSCGaxJQ}l^ zDih7&ZFsaHZ(YQJ#%-d(hrV>VDOduXzLMq4g!PD&2?y$$)wMy_LAo~LYW=;%3?EVJ zZc^Z2U7K|sqU%byX4@g$bz|X{PsE~=j{HCc4MnhDL6$eK<@0FsM7%H^X<0$*#WZ3I z#dcU1%meSe>xe|eT=}GQm8;JhV$61BWqX)tC$Pb-rE>>Qw(}4PvfY@s;n7^)x`=~p zZ0H4 zTtAE%KBoE^p}>*4j?#6st}ElJ`dL4gZB_lOf?&UXEN@=R7qNasyzn)oW&Laz(^!%E zNtpZ6`eDp=WtG;?Rg~>QPqtgJAem@u-iAk8^43KhWcyRemJikSb2Vw~;?ejzQ)Qy< zcpDyV%Uc(5pmC>Y@S(5zsSkb$ow)||)EA6Hv`n}vBfGv}yf71RRbQaN6Yc+Mz$Es6 zA|4X^&yd9a&x-{c+yB)W>g+$Z*qHsti}e68hHk}}XEH!ghG!fRL|(kfKVJGMR3i2+ zi@@4`Y5Guj5gO0sqtIf86oW=ODU*PRRp2I=2Xa}y5S8CeGMU0$@W#sDB<3-xe74P9 z`)HfWAC?nexrtYw`)HSy#qbI;VMoj#-tUe<+cbL=_zl{{MXDFqvQBsN>hzmPggV`U zx8c$Dymb+WI=xGE%7<#bxQ;aT^l0qNRGDZ#Z^NUVc&4P?`B*9NkCkTiG$Yd)wG2J*FS z+`N6r%+Y&o8&A$rqr((_h`z8e1kRy$M$bA_YgGzK{5Y(A>$_Uh(EF(}6kI(XiI>$F zR471b*ew1$iWOAy7qkApED60&aG1l<@p^fVWuTpEu1X& zO-eY+x=b={*}g@SOJOLnv_S$SG&X`vj}Y3fmprsIt<{sc93#5cs8Sg6UA?4R}M5Uu?0wELI$GLym@VcDbqGVZ_7D_0yB8-)h3ciPCKkkphYvFuqIW$bQMeNFmxwl zjXs6=FI8g|2WnhZ9;&g*Lp5dy)wr+HBx+pCXrjg`qc~H>sj>2u+u1Q z?VjMxlB?F^6rWV+SJmU2Rkw80Jw2xLu3<~Rddvi<)Gc^JrOLr)r9K$zF+)(J zym@-eWT_r2n|hK61-1aJ(qrW!;#GQ_qLX5;kRIQnveIq-Z}gbyP~>fRLy_fov&av5 zddxtSnm136nKIR5dhcN?E_hp-;22zb>2VTCLmN+z8Bo~3*JEKgI{9jPtl~hAtI9(? zR(Yt$451$PRhmSPYZ*=SIAt`|W92EyRqJtzPb&1Q>hZ0rTMlPDJ?0=MTm_eY^_U4z zso&rYm8yXdEA`=6j~Rj*<;~M$CQJ2L*;tJV9E;E@JytFvUZuw=Iw|&->M`Ee2yRnZ zw?{5DdOQ$;y?f#sUz_Pr=%D<;8%fk0R9i#3}=nx2h0SUKL{5fmQ-4Z(l`l z%Iml@jm2zs%3Z3l%1AP**4Py9s?OI}oE#JEj{pAr2fR`g+^#y@2?BBYx*4Rw!I#FO zedw#bZN&tryal|W@-#qW7~d^$FE-^?^l zCL+i&#aO1&l`oc`*1dcqM#W#1%BN^2x7d$b&#yr z)|$5VOkxIV9&fGJ2LuzA2EoO+4;u>jn+H|)-TynCjBx{A?;mjq4)xgao55qp*Wu(J zz?<#3#m2b5gy+5Ba!#1Lq`U_sy3Ah77@HxSxdgiW_lfT)twQ!9VI192(!VasNI3`3}}6xHFvl6-MCB_)JIM z2t#r8?ldZr=U(Ht62^Cof%6H2t*;1z9o{Ci&6P3qNdgVKJP1BUMs30G5yw&tLW%?N z9&a}IE#iHM1(9s<0B{EFnm}C%v|kc$KImDzmym7`?4ig8S0J824__7pZw?s%+-ZP} zt6Pzl9AtylaWX?a@{N_o*M41Eodaep`7raDOVvHLHA;M7MM`Yy`*1g7D#!EBV5&`QDg zK<`RyOJ6CN_Gg2#!O`c&lmobomPa0CGZ}QQ8{>YEa2nko<8FkkEX6tr_Y=@b_T#ND&$_4Ge3xe;yL)`5yG#k7OS(#qV1*aE1$9U&L zlNc`>{4uHh-~8O-J(1*=3qJYS=s{l~7n9Y9r{j8S17)khQ!k9UFQXPrJ6}xn?*`1i z818jwwv~6;i-sE&{0Vt{YiIH@36#m{nBd`8gW!+Z0f7E|sX?v5E#TD3`-CfRYtTG+ zfWsY*sd5Wl@9Y@*3=$_U8=Qi8CWB`oKZ8C<G1#s{|~@rJA(1cP90vcda_)ILI6 zHCgSQw4Pr;*Nm$peq$0}DYyeQXga>ehp~=_NjusQR4>1dC4U%{E$uc4H9dXn-+>FA zn&|Fgq__=MBpV!ybbol5(4LTn;qHO_40i>j_NH3iHVp$D^fqeCC^siOZ@GJ%$0{hf zm8b(|*tmKFrL+{ZI1M^JDYyz{yXlXF9)+Bbf1maCI?A?(aDPR_aF z0F5;$ze@~l;Zdl8+e`+dz;Gaq+kuSc*<`=-Q%z*{H6MEvmZ{ zx$(EC?$L7$njGBvPFynspdnUAmq6-2?4M#POMek_a0SX{oE{Ahn8)}a2Z_G{y~+l|&WyPqoInep zSB@9R-qQZ_$r$N8)+3cR9O=HIg8MHyO*^8k`W@8U} zG=|>ZI);vd>=-&e*!_(l*b4S38!UBpZJnS4&$Jfn1gjzrp}E1yuc21|%69ZJG|XDr z+X?im)vayqnmN(gl3g=RKMC~91|yMA~k8=f?++0eS{KQ_v7DM*| zx(NT-V1L)TCk3m+^F0KXB^!)QYJUpiSRIW*2tDah!RBvT3(W;Dpd9oL#{}QrJ@&)i zgHBRov%zrG?o%)X$ zI>X_z!DWs=%6%o5AIE;^BQQMP1~!(zEfUP038oHX$0HGkb@3Tm@)j6-4ne$O!Gw2H z%t}Z-EciJj6URZ<3d{v4(M!N?0cowRRC(BiBO&Q6;0>-T%nP^1n8En|4Jm)(FslX4 z!3M(o#lz6w8y39gVP*^Su!or=Og$vVyhvvwVeata%oXMYXyCr6pDhq)o-oJb$uL8L z&4ih_2|SgrGfqpet;NBUVa5hK33D{0YM3>GU4)tQzj%8O=&GuweSFS6xi>d~n1m8Q zDWND`dXXZ5gc3}JfCxyjfeM1ChzOD(L{K^i2#BbmNUw_0B4X%B2N7N)O?nZe3G#nt z&zzg&zOVXz-}?T)td%wEnVCI%@7c3w@3T+2`-xsa#)`$4%AYr!mF;jp7yr!ZVL0)~ zzvGQgQ+nNS%KJF2sK4QiMqccXD2SyeXn^4qeMmULl!zw_*&8=}oHjI-(%}2;S)vnS zwWS$GXDJSa#Lit>W;p$QrT(NZ4QHy4V_Dk`=e=B7usdylyq~ZMH z(9 zsbg9y0-Pwrc>;ZBod>Cg)yi;UpwrM;u3@z{c5cG+%;^iA?q-VB93`ARbl4hhblRgX z))_&^z+rD}Qacgg>^GeHsEc(Tq~q2}!>IzD(_eBg&RM@2J3pf?=JW;Ug5flRW#)_^ zkL$WAwNOh%faAixJd7TJW!8C+LR}9S&Y+G&4`1XM3w4DV&SA_8;S_X57)}?=_nMbj zr;w|t;jB31aEiN%8O~j-c<&%n#8PcnDVK%3@xe+`&SS2cMrSVe7XLzyils)b`iAoq z)(G!)=0v-mFdP^16>}o37hGcu=gVB235HWI7iY5Jyp@YH&2XN|#hGb1KPu-VdcpOX z;haMxICaf2oX5X$+Vg^IzRQbVJa|MnX*9^Sz{Qzu{y|446&&{J=SLmRd#=@{@5S3Y zoYk&-MrTU`R{D7E=^9r6d=*Bee4I>Ih~ZT5adx^2+g#U)FL2d>wIY@-xhfgX+dj@U zS7pQbz{k1ns$%Q}VrL+w?zkQ`Ix|$KmGy@U4@HTM$47)S z)!J{bF*?nl^Ni6sYHv0=>!8D&sn$h%i_xhHo%}{8Jm9?1`3ySDnQD~_xL|b7EhhTt zH-cAd1l%&5YVd||S_JT13!{{U4yRMVeZ%6%XU8#z~lZPblP0vUWB`!H##wBk8qxJzhyYz z9um$KtE+pU;rz?T>FFL~I3qDnrPOQgp@!4jXJ@W^q~Xl>)wRft$7)e~QTSO(t#*$y zI(L1RGu`72=aW#U_9O1ehEs1it}{&Zo^?+(oGrK(70xa9OvABpy)7JQS-&`t|qCBS!=fE0=)5ep}&33k8cPu(7o-o517U*!= zdkP!Qb<8BudCSw;a9;7XXQXGa;k10$Y0o&%5W^`Ak4c-Sc+w1KjjzEmEI>BEoeeap&E{GLm53WM^FO7D9W0wx2 z8BYo)mUby;^n(s(x94-CGug-4=b7uaVW+E)bHFp-aQgc=2R#c72k$PMzgRltSqu(# zo$G{y9f)VCJ0I-ueGUFgqo2WH%Wryx6H6yN%M1rEw3)wHI^|j6*XGNfRc;J0)$A!c zx9O^9wc&Vkan|~ky6VX^mQ(UOmals@`RUy7Y&JR%c5!rWd$#-OJQTRg=(G=ZbRG`; z(QvLuIGjfUe=?jm%Q~D2fyeypR0+)X<2)L8!j$4K;qhNARS!IEIIDvkPOZQ*ZWp3u zU3Z65C-9t~otVJ$ew>zpm)-2K4g00k9*Pb8&2V-gNAcfnic`+2T$~$5=WZ^}9m5Hl zCZ+bE)SrekriXBDQ<8G-_{JhB(B*NX&0VqA;=fqvcszOIux(&|54W%oavlHOrgnj$ zhEpLI2lu?8(=r#Qh~b>}wXj{_Lmu`|+sk4n7IsP*&J`c$$-pv(<38``JQet`;bdNO zIGqA3c=ERJ=|H>%iQ2o26rDZvY+y}KFrs(Ox57!Ie#$xep~HDwIWEk1{!62w%CWEt z3+Fv>hQbaF6;3RT2PbdsGXtYcU1P9AlTvd7TN}<;?CFHFR5^ovoUfJhR-&V`PB|a@ z>|`mYU!0?}T{#)PQhSwCA8RH5rO|=FC;VD?_}@6E0-y5JIUCr;!}Glk_8a^cOXmZ- z`*AJ@zGyfJzT9#x@KwX<6YZ3`75J8)Wv@5Q=;Y*|eBR-PlQWw_yzlvO9`L^J#|ib0 zHj%(rtkS|T?>N7@3V1&>rQE)(RoMHHpH5Nlbfa_FcG_IR`>Emd(9D)brM$BZXI&xD zNux;Rlq%ztdPF&qzN}T=JJ*yN9OdX#^nT&t$c*RokH0Cou zyTbHzc9z45^RDshMH}y0W2a=Gqm$y@;8%M)@3)4tudbuh-MiIrG8;Oam%V!or&pB2 zdCmKy;Z*UpXQ1~NzuMpNo;5nP^Esu`ycZ4UC7(Bjd#@Ny-y)9ANbm24li=(72i}{8 z6a0XqGtPV0aKf8AoGD%_kRx+YQ-?F(8)P{DYUFS-z2Sz_s+GgZ_7*doUG*H!O>b$# z$%(^&po)Qc=dwGfve9|W7YX@;Y8lRVH5@yk;P9B{tZ@$p)%COUU{C{7YPBy8iw89} zrO20`O9nMDI#IsWs;qJ@`NpeKP*Xq4b%SC}sSN>6dm02K7|!s5!bzh>K}m*lCfw0! z%$(A>cNYVLIvAatIXEb&li}P_JF)am(9?#~uC!x0EvTE}H1lPEq0Gr+d2LWnqvJ$o z8f^&bWjIb}o}f1k=d>@P_6H5{D|I;NZKE^FH@lAp zrTXa{3mR&4@WQb9i>04~h8xZTUp$`(de5)asi2WY#~IT!x*0UXaK0|=_~)K-PW!S1 z1&hwq%8pJz@Jv(6iNiGVDCe4H8(ilq=g8Ae3qyiGHKp$OIN`w=el08!C(8;{&?^j zqtmUFW4U?ocYZq02X8kz0~$CwJ%V>hsU{BRwctI5L)9G4o54RCPD~w#lNx-~&+>c0 zKO3E#m1tD(DZ@GD%SfLGU-Z+-2)=AMoqaQ9QSeQ}+1|pb>&xK#hI230;baB}dO0KI zM9cQzP{Zlw>&1@XaKmwG$GRL`%y4pA_#<=j#QZP8rH#%NUsgX6T+VQE_LaP7%| zIh-;+PIXvjog^Qp1~}}or+vHkn#%da$Elm|38Rzf<21;ZU^wG_oX7Je8%{$Xr&+!f z!x`-3w8;0QpHAz19sP6?@;z-hReYt|pM>{3dDK63{5dZIn_;m<@}ai`>$_Kd?R zA5QcufqxqH)dF3P@^wL98by>@$D(mqVr~W{r%ouqI5~A%q|gzH$0?qGeV>uH!MSjB za_V&K3>%`}O5fnS?6}CgzF25BuEZJF?kKo{(kP{T0>tfQ`Z#ETrJ^@~GM*N|b$v%% z8%Kj5KMTJDiFUn$dTydWUmAI7T8p!hF=+P-sE7M`>J#aQWB32c-l?7JZ;R@pom~I= z4+U5Jo~Sk2_u>Na^NUxceO;7RMwvv&pH$ja>FupV?cw)Z+Ry4earX^+9?)*lrD+ZN z^E~9G*K$1`3i_DsdgxD#TV@>(we2c2M(JaQ{&l&Y`=ory)j}WLA$G4|uDpb}82lvj zOVgC?M1#=3WxJ(6(@gt3vn$#)8P_f>C*?0ye(Z6Pd$nG^ zf5iRSjQ%mruk8p{It_8i{ftK(vEA-3h}{lKW0l@F{`Sx?(@qZ!wR!yUoDBNG?RZc5 z5wD1yQRA4}6g*YhoocX$PWG1i`n@dl-5H`k4t%y(@M*DEe~i!>_l4GcLg@D&NPkka zzTJa`U*{d6$I&0QGgfIm%syO!+sJ|EcnCC_Se9_Z0uEem<*sx5@`9=Feqvd&j6>K14h+{u*)2w9!JrTYeFm zi8y8cBBlM`6+C~r&|x=)wzw^{4CXcK*He1u8R^GNr5`9w)B39K7X3zeE{@CfQ+{dX zS5W>jje|8QGOmx{B$V~b;-r!(Ki$Z2R%@ux$nnB|N5>;lX%)l^`}b}2`%R^l)i0%# z=G!CX_stbr?zqsCN5y`5^=lZ~!}hK(5_&-852-vr<-e+Yp|*dzQd{e3G*9fmq3!OY zbS~Bb?pF=?(bY3x1&nGQ~IpZM(V$)NmBkXoo6$@W2E6j|pweaHT_Xr;j( zx{G{13oi!0g?us_l;d_H-t6Y_o)}V^Dh*`)(p2#+wi98VP96_G#9{o{k0m~28bF!b z@N7wR0PQgp@ecdE9$!Sh=KAopOS8_tev$bBR2zQ285BTu597Ou@b7DekLRUK-n8hc zyHdWQ(s^dT;IHSQ4%&|D)9`&Bw9DN?{N#C7=$VN^mrf8m{215kp^ca~JU`~`7g}J1 z@UP;Gjpa)<|5j3d(EGyQrTnO6Qtr%7p))l;Lv{(?ra0yg!Fgx^O1COK`<}$x^`SM!6t(F_0cD~f|6SQ1uEjLlixwPD) zS}sw`jns0Rv|L*)_q~>DpygiBa-+4}PA&JDmaC!VKG$-Ev|N;ydsfSRqUGxRApI?y zCbXvJksCtOS|QS;R855(U=oy6{I zO3Nx8qx4`$kw32Zvf>VkIo~ag_E7IWoZmf^qI96rZc3{u&9Ahn(h^GfnV}U}k4udr zS`9n>2Ag@|8JEJeed?cBCs#+ePmSLo^vNfM-o(>_tC45V>ALgLVs5WRE~WF87BCdo z0kF^OUX0QUN_W8@%+IIz@+U-x@BxhpE z);zFH>1VLV>(2_Mb(F>^JQOV-qsz@3ZWh6VW0ELK%GYyx(a?x=h4h} z1aDLt{*mC08izDb_9y>j=S-a1RXd$wpZ87nPUc%Q*wk;)ZBxHRf7}rLzpuYEW$nYW z3y8luW}jivX7$I*&xlEU){&MRiD#5<0#lYlb{_UlaendvLt zq}-?sp}$@d{#@wagMQaHnGgRBmGhZp$iuD}?2BI!ez`ySJOS6mdM@$}`pf${T9jLE z(d4V*59d6h?w5R?V^Q&gY}cY7rB6LAI2-#SJ`d^xKA#^%W1Zpio^;q@{Ze=mglXUx z((Y?Gcj5jN_*mLM&GZw`ySEoRci{)NS3%pk7S~sSF&5SQ4bKgsKOIbe;E#b)zTIh| zU3%c{ALvyFjR5@-^dZm@YltF2w`jR*v!xwH4oLe}!>`<)u}Tkv^7+Fx%rBwVE!{6 zdInD@qTh3sP6Dk6{2FLgP#y=iC;deJ1)u%5U>)}hJi~PI+EBaUlM+v|pC5U-c2!&%i!EMce~)=pmwBpv{n1 zUIi_#G#2gd4?L=$)Vo5d>ovg@l@3t4S!t5ePm~_fa($Fq=qH~OCZgX=V{yGZ2=zGr z>(LW8+kvNmz6W{)bQI_Z7?1IwPFy(yWl5R4_P;Ugui)RJEU|A>@r@gc)(`7 zNfzlLA0008q2@Z+L(eEJiu}pts$qP(;`;oR`FQ65^^61m#~8sU6`uut68j_8Z;W-f zqAl|4D!&c-0mhBjp#fMw`J5v!&f)(I|FE5`HbOs8`g)3Bey{gW#KDxYcnY|A&h>b! z0LhzWPKaJK_D>u~&w;W%K3Bb;ALq482sD76SjBz_ps`A0K>6HYgrRs>75!aT(xQ~7 z@my?kteg9J`~;T(z6iUFkKE&Oco!P|W6Jtva_QgdFM45bGWC%EIqJVwNISn!njia0 zZtu^?+f1)5llJ?cBRl8E&N;4guIqn3?VLm3#d^Z^JLibbIj3`;=$sEv)cSTUm3p1? zME`R}=Umx2uP%lB%J%>1oG@JV{^`7MZy)hXn?2Hwmz54v`Y)AtRXjlH1f~Axi_ZBj zpF43}IOmLib>8WmA3Eode|2u`oJam|&#UcZ>EAhx^GO>2|8$NWJVX3lE?vs6SL*&! z_|AFh*PEoj&iU)jOu>J3p1B$OJ&wDtl{)t!-o(7-{o4(VqmIUp77br1`|>Swv5p|_ zpIjjJul+>ySY!n8zP}3i2PVt@CLGuM?Ef=Z2benTE{e66>wQu4(TQiI9d~s;R2w3A zrRK9=Ul3dr`Gm^_D$Ue9w^!+ki6VD*7kZw^Dz< z6h}L`{pYp4!`1(h@GsjPqU~F-Ozan>%AY?@?0VI&#g(2NEaeXTBJw^{q#cP$ z?@p5TT~fMQ^`@X4&;RaPK2-I8Rrx-}8_<8&Ypvro2Jyu0tTsXDRpe*JKPlaDT<~3` zH=h*zuP1~SQXH)PDz5lVok#7|-=~%Tl3CXxh~xD*zGJZ*^fcwHBmena7jJV`{-jDt-%D8pZd=i}^_MTlV^n%920gZ2~bN=g`^E&6q&UvwO zF6^B9I_JyIIkIzJ?3@ET=g!XgvU85?oC`bW!Or=wbKdKm?>gtX&iSo#UMuIb|2|It zSMl=;_7^-pAv$0E$MroOrwARV5q~kRjs9ZXoO2!LJjXfTan5y|bCkb7-}(3cr#&P0 zrM^P`$yW>4f!Tt)B7gAxs;bD>HL#B=gYjdV9O&Xrc!#(Lk&RiARZsG*3>ghW!AqEB|df zFTu~;&NAEbws#QXdKmc6BCeTUxh1q0_}rhi;4|$F%Kc6UpJ``Y$BhSn=N-oX+|G{< z^Zpa}cfObTU>qRH{?WJgZW$6m8tKbL6FD={_e)+>g zt#p1?Lpymtxd7z`qJ5V!j%?=%ov)sqVz1~Np$9b|Uq{~I{Mx95&^G0TexdoQV4~Q0 zKxr|IJGaL*g<7LeoV>NFin|+%_XZvt{cuvLWVH}Mf+OB$kZV>zDlsa}t>-;FL z^J9$8k3Zr>->vI%3o}0ur>Z|d_0OsPtMQ^=TJ_hf{&CfRRrOD(eoxges`@Kb--CYe zx$#t`A1fW9wAFds?}OhTQ~IRR%ua}7@ZIfkUIKa@cnfGv%!?DCWw5`w*gTwy?O{KK z(JU%y3?sw=f1NF4X@oTvH^^T7J(M8-}%o`II$U(}FQU0|w z8IM9(*SMblJT4Zs)P1*%pNF1@f4Ch}O?{YuI^J7#9Bb~xxmF9z59AG=pIzWzrazl` zEdA#8uY8)%A<-{Tj-!!EGn5WiIX|Dl@;4M`sXY1H+BPv ze~MTp{^+IsPhZLVPn_Rr|N1E{VEm8kRpU3@e>Q$Yelz7fzI|!@pL{)v_|<;?)%DjR z_=ElB)YlAol>O1el*98{kpKVpT$P7LnYh7uV^5s7AzrS&fiDMwru1Wfl%@utJpS7U z2<_NkC~5w&P@etI*KJH|-xW$q3n^`(v_76+V7<>2KY{pW9Dsf^T{jqhM|`xxd1-yn z+sL;~LEX6T!1TSfGCu|?P5eagY2-7`V}mhH?7x3HKW{!n^quqf4f`bCh9eGR(f@B? zhx6(Kdfi_Pd6eychy5DU-1|k&_q<`$eYCe$juuui9Dad4DreGWs|A zB46KFblKz?oEySlTwmN!_K!u6A}+XH&EO}djp08&e+Zl*{*mhquUAmUkB#rTH!q{5KH?1yABR8qivrA5yUX##FPY&32rF zg8$q)#>uI@xAMFQpx;d%vuG3YO?gjp>Ziy3)~w)Ja8V1Gk(J}qEqC0*nfZOT>t#H*B6d{L$$lW^atmo z!0fL|%5O4R>Kmf#+w)5O_pz2+4kT_h6jW`-IX*+XybA>-1fXOaJ}nu@^)y1pefCmVtS|>vKc~?sEf6e)5p4yV$>B{;d7C zetGCp7oH~`s)KyZGy?XRI?q3i#&r<4Gw-=7p6|kX{5|UV-(J_}y-xQKU*~gP&Riwb z$=~}GJ9+$?Vke)UQ0(OOZHk@zei?a${qm5B6Wp)zo2N2Aow&e#GRt$=+X=ZT;8$au)P1D;pcyy47S|M}lc^GQRc4X`e8I~OaBQo2$ppX>5C^LZ2N zl}i`O>j&edm|skfE*0vWH}N?r=hvsuUY7H@6jMHzVp<*jV_FjXFQ!iYPC5U2`T0_A z2S3lu^Ng>DxE!AwUTj|4cka*UE6nF}>FO<#QxE*gafSUU;)d_e&-jIDn^Ea86CTE|z0r_Pc=NHt+`Tcd&$8@pI zM}97z=gUQ$S1|1`N9JuW%yXWv%i=}uTrZABJhH##zKlif#<4t{_WwroLyK@4c#SFc z&+rf5w`q%b=Y887TJC4GkN3SLF-}i`9Gn* zOus?@nLaR3=wU6_9{S90th6co%Gc|!DBX^BFunrJdQ0GErc<L%QiQ500+Kw01-e_&lJmm*!d)(TdKFa@9?eA{(!yn@MpH#9iZ}pYS+eonEN?a<>S>(Wyr6fTsv*|rPoAH z;u8DCB*~-y`M#h#9Fk6kwVASeFuT*xe` zXUh$t&bdz6j|4mR{_44*{`(|YP)Rqg1os#B=N@f4@Zl*{kHd zF9tZEWdywl8q^Z|DuZiT$ynbXXj#kpu?D_T1o}v0UI%JfAA?^MIIxoNJGT-XY!j7e ziRV0YeGJ>p`KvB{40##Y$It6h`Ids0TM?8RNHhm@R2e*<;}ZUtt_bQ7gztsAJoG8{ zW0Ni1C&qIPOzVv_`O^3N-<9cTPs{eHpNXU0faQ`bOi_|6FBE zio_3}ukt$634X5w{SwKNj~$E;&6($b0@@1y3GDkgkJm8|u^Fp}JDpQr)jdiS!;7JX*9L`oBmw9}<89#T2_upE` zdNnmx_>WtnUn*Q^<5EN&VYg64?iZfxt1kT8b;Vx4hElFR+R1sO1lq~8TT7|GtV{HN zhdr+UGVHPZ6WC+g8*#?8X+6<@w2{;o)RgF1)H?z8dV`*Xy?&rCz#cyj(*pkNs&V!v z{P|mDJa+?n4DtFNXf5<_68pJ1zM~5IIbr_~S|06R3)&Fv-^>{8-wS%E3DzI@bLw)U zbD)`+50`!Z#dBO&iLL|BneH`sh3do7q;^2?=GWPi9p$pf4rCYU6y%dP|EL-L9 zk0szd4E*a#!!iFJX;qr!`NPt*7=GaA0q0?Tsva)&R)+nzP+xWUV>zEM##prUD84BB z7r3gQJO_)tCa}kOhWo*Jh3V(PLWkrN+Nu&$X?HC=Ph2Vy=MiXkPqdfmk$9nzaYD}{ z|1rO+>IEu(7X4)Y>q^^d`IV}-Lhandc(L9MQx4YwneyD;w@UXaovE}7@)AGSmk5gI z0K=j{@mycn4y=1j>uxoaKEin$zi+z`e12Z=b6}=dls&6;FY6s{kLz^!l|JD1 zcRi!T|t-MagsVpe`6jlh7$7h0$(=X^Z zKaW{Q{eSic?gy^7bbJ=+xMe85sx(x`?I^~fEyjuMbO7b`j>jVlxs>VRFU20`t*0Ts zjq5e$U)6E@{JQ8rGhJvqrQSaT-_!94{>k7lr@e(yp4(YS$GM!2b1NO^=^7s|>v*5i z@#gYeZn4^(5-Rn?shumMq@D%uil4G~ihpWhKf~)P+DK3Nsd^@CnSznJcNOz6W{=NLDR7Wy&Pf5xj~rTi0E|G9i+%s;l5fc1Kv_Ioq> z&G~c(=o1l4kr$rEdIq}jnCwgC92dVXg7IHg)}e^WH)Nfc_Y=jBKf%r;EqVVHL4RPK z=6zVCxema-O)0Ol*;;?evdHU&@NI3-;}{R7V{78~g+MFRXX?}^{+|Op{hHk8>wx&1 zgZq6cpnMKgM`>TgHS>FcGXDvszbzBI=&IbO=XRWeU;b{qS^hTSi|?lo#5kM--iC2l zi~ABaYqK4T=3*RpJnLc{u7N&?ap3##l`sx`y*wJ@z;V2aRG8l(@zz

lquRHed+spC}h#PM2G|UI42QVL)uGIDxY=CcUK<~oi z($1CGS8+SLpuODA&1ml}jQ`YR@mE?~p;famKCzh38`%%|Ef5{gR^XS3mGQ3}8$n*= z+3L^>#Jb<`AKDuQ`HS!`UnhQ#_A)K=sLa0}XfMa>x9~6fc^lfx{@aH3vff~{m*e{H z#;-Z-*ML8GJf>j$c$~&!{7yfN{VDA8{`D8wXId2F*Bbi&WUmSAas5qUkLe=VGvf<; zjQ_Xg|Gu2~Hxd55h<(+?<`MK<2>gTlUCw=Oxi2mABnAFYU5DT20(IoqF5*5BaI?kS zP7fVJd-*>5+zEomVg5XuOW(04&mVYv&mRn&FZzz%ztXz}pK|?Y@t!?XDfd_Y!CKKt z?$_@$6!~wonhVMCq!@OU1sjriSZXX_Nf&hrv4YkzsZr(k^fx~eDW zA&s~DRYd)liF^=QVC`kb0V%_IfBv+x@6% zuZJpXyPfeV`ybllTdoT#8jANic5{3p4h;SG{y5|3_%TS!2Wa_+ z_el9jrMdms5AB!-KXU#!0Y5S=hqz)|0rQG!BIYgAeVS*MVBYdPIfXpKv^4S}&!0cT zgjPXbWb8&>F0{i87$+m6#1qI_P@Mu z>#zy?Q}!#Kqu~9>X!Tz?m5-Xu{r{`^B=%OJouSAZ{G9_g`p4%mR}k+!o&(T7rV;2L z^Oqyu87E^Oz;t~JrZNuSpnurf&<4=L@H^AV`DI*Nd?D@ai+Z-9{h>Xi{9na+wWSi5 zl?=7$&Kmd?<$hPn@xkk7D~t=rMKNvnPl!uy*Fm)VC-iGL+RgLr0NTwo0PSWPg?2Md z#XRNm)iF<*Uco$N8i9HGJ?tN{&xw-#k{u&@Z?_a0<`TLi zTq*2vyB1*HGkvm#*y)CIByLAzoFg%R3+yqS0ek#h!gknWd0W_HTDySIx{;!{QSB{< zJ#P0x*kd{l_7+Fu_c5Ca&V)VY-`DnirtNzc_PD-2u*dSBVUPR$U`?T$)ZQ9xUxL~@ zqWvx0O3LqrJ+ALf*gF-A_u62O+j|f8`ssDbImE;JW`26eiHF9R&m0f0Y-4>{w|U)v z5c@c${}*w>=f#nTJKiUaEGPY}gE--FoTBl~`^L*?H}7+=gI;PT{V!?a8^0J7E9G9% zI5~wlVZV7Wu570U`gt4W79vh~|2!FS!tzCk6Q)TzuD5mG^wn{=3VU3BjmF7A*kiwa z2zy*^5XP0`pe@FM$E794fyZgB+8eCnuu<(b!#MDIR943|3gf_b_Nl#3VUOk0VUKAl z>@f|;xU$|7wRZ{jm_J4BC8)hlYVSALfh!?VW}_Zto+A6Q)_P z=j4k&5f8xu_-)kIa_+(RN9&&C-@@@w`?InS->{Uw+x_?XD*)p=M)N>DayF5;>< z${j(zXMZ+B9$@~*= zaT{Rb%f8EMK1wzKJRM_d!Nmb zc~Ib4sfY7i7w|c6@_fk4m-5e|d>GcZo}e=t3*A=;zhnfw8{@$II%NcJ0!;<(4}GS` zF@8+nt}paH^xp?Qh4Es3FA~}%OlZ%Nijfyt{%4T%YlT^FaXtDZ`x)m&N?(Vc7{~mI z^$`8ohP81j=-a#TTn*^pzQI4$e-`$5pYa6jZ9{usLO%Ne zl<%J~Ke$1Totd$J-7bGSyszQ%3y>F`cKluWe`@Dv+Ri4Ah}~kCm$~isnU3?_TH?q3 z@Z(Z9-VX(>fcQNLnpPU|13Cb48i+%jC6$PZfrcYqe?))9-}vsqeeq}R^Q<~J9J-GB z`Fi+Q%xAv-T?0Qc9fAHaO@ZH;1~kYiM+u0a4deS4HT)652=kMc5 z{4o>$*peUjXF=02&-glERT05u5C@;b&fmq&BFLYDzxaEVtDw(xFxt!hh=9MALw*VV z;`Q%a_>0Fc8*#(*MZ^vJ?^VPNV?4%X=pp1!#{$O;Om5&u*3V7Phf}ne`jYq>^SGb&UtSP>~Q~P!Jq7p7hs2JC-vvw+1vd; zuye@2`?jP;K91)JlA_R#Z_zw5`(TF*&P$1hjl z7mllVGcP5c&+7cXr1R+}=t*7Aw(I&hPU%}IQh$x@oR^&aGp-kB@O;O6L%WIYqI{R{ zWuNgDD7U-T388(U$Ls2iE~5W1Axi@&)2|I>E(*Uxz+ zAnw2SgVUcTH}bY;D&{ozcTR@zop$}F`hxx+*7r~T&FxQam(yly~HRRsP|BDDTMss(ht? zDDTJ%#wA4({?ffOu;RW?@S5TQh`Ni&p4|i&J&{dRA9of97@S38arJRJ+@{TnD~}|q z<_@5{inlF57PbQDCG3*eKJCQrodT$z;$B~}{Qw#$SnUVUID;MgIE7Oz_5-O<8Mc>2 zVn2|E89bTRWA;>w3#3_!8!f`!Dl3R~D;|ScU(XJvfU;bEGBMui$xqb{9#1J~PxH73 zXp_o6y@0n_%7l>@s|)MbzJ;f0;tEiT;$q06&&3s{ZrHW3yg%yO;)$SnikD!w`f^+m z>hOrjzrKpE=)@JJg^H_xL-cmsLsS&2EtemQ-SPWz#c4+_T(xE?I$BZWdyyUcTBV7f zwqX6Q(EbT=WoUq6mT&PqOgnPP8y0wkZaKI$u2xG_q*1uK;_^4vGOkP&st8_gLaOO=JET@T-nUDRGNV^T5Ov6v%^vBbN7Gn3Bqn|>n41QP3r%mgh`wbpX(`Vx;x44(+y1|oa6yo=4 zTrawb-BFJHKK$d=_#0nz=r8zH+=6}RiowqK^q~&eMe=V0JPZHai|a#wD4u8h)rX2V z5;@JpT~9pP%cq5`KO6NIiGP)r8k|n=p#Bo^uMyux<#^cy|5lA3M17+fOMP|Y)2L_+ z<23sE754YLbXf6pQ{QmPXwLHSbo(^sdwe=wZz1^m^_(9@P-IKNZ%p8Le4lz6oK9I2 zvHOny08hrTJe^*=h_J+?Kpk2cY~l&OW=fd`r_n6LQ%HePbo2?)=kl@kXet*cc+_O% zm-x}tNb!V^IiHTEVT!}Hp?t__8fS1C4Ys+yF;qQ1cYR~%jKNNQW9fQQ?)t`2qvYK6 zjWb`-VE=fahqGDQuX3q>92IXP`cnTms$j5F|9IlBDRck%ZIf>CfOdBpuoLHh>BPow>cOU%XB#Nwxu7rSGYpGSO+i2s;Y8k|nQkHTG) z_?a}XlgO6_u>Dz-)S0mp&$DQgV0tJ&vQPXhig`M(e1>43Jljux*5E8_*?F;lK`$C&3Ve@pT9#)3arTpII^5d$EgzZmNys3%c`HH`a7Q9w*LCjm$-=lc5CHREm z@!^7hSNvHi!GSMG`(MVo%;k$Iu8MVoab?9tkY^dUR2+c3#kjA*>6EmU=YIx`FgVNF zi+p53UL0}H`x_a5^at}KC*Ee$Lc!GGChO0pY{fgwe3;G3_{(x1y(@ADZ&Uo6(a*Bp zIVbX4TE5ao!S!F{5>Eb}O(P6GO}}aR8NFDZPH&>Bim3lv!QM&us@D*jOIsBWP|ROz z=lR}Q@sEn56#pWaN?zjn=TiROtUpZSdoD%3${4>TgZO0mEX7L@pNuEH#`1~OJDAt^ zxzyox(LaUw$C*D(Qrr{q&p74{k+;VFU~>Ey)VDw5bnnnfykDJ90~C(~#;-P0nquA$ zm$w$sNJk$Szw<*A1e^9&q=mH5;4JGqGoFiSiQ-j?R|?kgT}-P}r9Fw`xqVB>J5q3g z+kz{MWt?TLM4ZIhOQ?y#PX1X!&l`;SZ~C`{-dCKzij<$J_;KUECA3WOdgH$(v`O(z z%x|v0$~dV%9`l>=bBfnC7koo;u~=T8&|b8g#|PUrnBrK>hat3#$||0+iR~|=M-(^s zj@!44Dk`oDf8w4oRS~S?v7Fw-xO079V}D`M3L2>RBgKOacKpABMi`t%A0eJt|D@=v zy%khv3fpt^SDLq@*?w!Z2WMT>)X1|eZXe?q!KOUpZ~V%$HI#SDd|pYLwEVi=-2as# zhP(jwLtI||vfN&*E3tMCt_pc>eCB>${MvgQ-#PkSK484cXK!44w*RFMKQxcyAqSUj z!}fA;{*=7<%A&kD#IJl2zw+c){)Auo8rUBVL3<91zo^V9p08ih>Z#nsbUJkrzrh`! z)1C*j^0xo%Rw*wX%5tyw!#n+OcJI9XedXo6c*u;r_C7Q5m{b2?obGUYo%*Vn@yo&W z^W~Ml;-_E2Prsa>esjztcP{%i4F1Yj-?!K&;XB_xyu%L<3AQl*a^!2VzJ!E)?bCk- z`9CBClfwLExqn7J3<+7|!;?p`Ki2wizjxUmIe5pr zzbyAdAG5zUP;IeKH4oxE6T(CpGiAmCnq0d;uZ$u9Q{ z(`cO5x6I(2`VJeMQ@-fute;cg3WIa>H|N4D;xp-I)tCM5MtTTwgYwq5roN3-(qO0j zMk;S`8kG&;@!v>|6sHsvoGkjZ;W(ax3EfD~8JtD3|M`wG4R-bu-%)AAE%)ztH|u{# zl@xa_#QxhvH5IQa&GJpuQ1OXMEdQPh<1fqI73)TJXb$G~xcTV|jila^Yo&Im8ID@lD*3Zqf z40gtI2dy>uG|Bq6gBmR2@;P$;`V-eD@v(#Aml&+^v4fHf&e4C$U?)Cy&?|~1K6cQ% zii0tpLudz0Rh)|PWV}dmx0-@CD!yG$@FB&+8VkOlcy&|3)>3X?8pSpj9HDr9E5Vf& zM?;J2i&9)COmI8JP0aPr4tmLvR}lFS#eY;2JW28WI)cA&MZ|} zEVIO=QJ=aj|B-?fcf$2%tbK$ED9#tf@+0(+;^XjlcIZ!3PH}8Y#z(2D;v*L0V^mjh zMmXbtQ4__hN-_SKS}A^~BI9gIRvcA>@h{X-aV5lStbLrGQCy^v;FlE7M*CRaPw@*e zf(I&IfcCL`sN%{l!S5^1UqJ8##ib$z!_D~1a)0?ew+Bhs{AIaoJjeKi4@VpMNgu8~ z5l=G&obus1&$9lnKAdUr86Vzd?49-DeaN>_0X)k5m*qZV>|YQF{IbzUk}`i;?jc5w zi%i8YnEEgK@bjj=t3G_o;NN`MYsz2qVcV3y?!z-7kF{_3aADM+8$aSFFOKrL;bcF1sebs1AI|4jzJVW}?}rEZwQrxFy%v7*x_<2+?PrhtVA)UIrzrT9%gcW1K21_A z`zf+`Tv#sqDa)#!%{YtXe8OclH7VEG54o%~gVX3ft_SdKHfxgN0)++7Rot$O;BORH zuPk_{;*~W8|EV}0>oeP{enRS({hQ0`ebQiS1jdKueGSfO&s!qb{hZ5MrTVhpcUi0b z^uIOO+0VJGgNkK8=d#X=KK*%{*9(_**I;M=Vp}~=aeJKoi*3c7X6%e#khRs|H2NZh z%LiF|72n1FCDsnMjwpVr9Ls~P6N)Ee{}OBCBHR3BxyK_vW{2kBkDp_Vn*b_*$K+4E z`k;6uwkK-%MYNogZ#*6W5if5G-oMCX5USEV+kKN2` zFUH`>v~(DLMJqncdi@s5C)2)__*!dxgjE!A#rW}_L>JV-U$bA1zOz2S#bbK!yskyeMhMxXXfCn}dv!RmEi z@PSuwzq3IVs|tF}<8csMnJNiYtyINwKcKd?N%6Qoe7;fJ>cEfr`L3^OTN#4s4C<+u zP}_>fTkbqQsR!79^{vi|KUmM#2lcH%iur3XEazVn<@%Q)^rI8%TTwU{V%%#eo-$2n zU@gsslM@e3GpcgVQJ){=&OZ)+Cj?!-(EVNVdE<*JJ&BB^jq!gA`j8z)g74x}x~u4166X zp`$g+q_}k2hU?EU2~S%|CdH-GE?j?3PUvPGRs2;0JRP6#tku(`oOH74;p;jY)XXO2vI2tY20^^lxQCFROl-;C#T_5?-~uc+wIrQGdN=6&38Qio8_K{hAf2 zxGAukUbCtwj_QG56-;>DdQS0^2ESp=P+S=JL_&XSzv7ApziH(!VCwU(13s59z=~14 z*WkCTL4r+thI$8DCky7ZFN_9RYYXRKJd0}u@GpJAzb_*XF|J-L2M_fEKgf78y^B0| zHDQP~BNx7zkZPS&y!sy05{Ftz4~afwPvX1QRK@QiUP2RxTT#W;9&q8rbZfQZ-?4uw znK;7gRzl>A%O#Gq4l7=dOP{KVA6WJAOF-Nn#I!ZE`=b-*SkE;Pe8J#(R%9!|kKw3$QsR88dXnI8ej>V2e4$mLgWzn~o0hoPD&Adi zILd#TxYWvcQE&+Gyu{^JYCpleF5&)&)!{Y9o9LT4g7d#_Ct&=`TbWjj;yJ)!lxYnStoh?xYh*4QM&DYKa^d1dHd?a`mi!Dn@c#%C;d4rwy@3U&IHO3PFJm^C_=9PeyeQ*z-j;Lmd5;zhEp#d*pfx4y|G$GZsDCc)lm z$j2KKPg-{s&o}s#Rhxg-4)^cFUnJU^c-rcvxQfBQTAwOzi1@4KK4X2MxC5}8&R8oI z*T?+oR`jg3L$L9`o6cH|2Ih>%twQIll?JC#ixqh4px^~-)7vahqc>o0cj85B?I6Jw zjQo;yWw0qvk`FFhecv(I`##z?gf3eH6lVa3gj}}L6t6e(E7nNGhmHJ-H9>KOm$>{@ zYntL_z#$=5tEGTXmy?Nr}Hw3!CyctXqnIGyS`Xo1Eg05L5o96{NVek>9ey z6?ZiKzhxCyEaP?Cs;&45jNhTe+g1m`I)Cn1DXC&Vu_w{L67N`}6n8WD4{MX+*MUzZ z-nH&39%t}9t9F`{Ujlp~@lUIl;(Z3+w=xvJig>)1NUo!bhZ$_S!iSpjGzI%tMfd; zydK_3EbAJyP%y8DcM>bO0+tEp_3%z&Rae|9!Mq;cNv!D_^o`&NTW~#;RL>Q;PH@Hc z*q!VT$WPULh&UHAAticTujDievhGlp0#j|QY`CP3s!LI zSDe9;`pGmQSmqlsC9eQfpVLVDA94p2oSRDjsgu(>T{>20Pcc z@vfy>etrR>#!2z60pE(f6$U4}qBaW7jAVH`m-jotTMd57wOg=hZ;Pa-T~}0Ip&L<) zq;9U)HnDyhwX-?ipLLB;ycPBt&r*B<_TrNu|6c6(D}k?eCOz+(l_hvedB!igR&O@g z)E5=d+cjXj$hkgzTfjBR;553bICCe<)2KPhv;6E2jGg}XaTVV!@{?|nLrAGKK5{C~ z6S^ezaXEMd@bgLie7JQ2-Umo}-G?u4!>{Nj4e;Rx>$v>eK0NLK<9B?xJ<4}UO7r2y z1`qS$rFYSer1yMy6|P4|C5>{Wn)asC+r~c=T;ujg|HWUEU9{fQj01iYeerjOtE9n>KQml$2B%Tv z17a`fXij}>e_Spc6_DYYn+sbs$F(*WUYIn;bt@OfyTYz5$E5xx*nh7|THs3mIk)|# zu5o_&dUlTf{-mX@DyMVnZ*|4_;k$x~?e|LF>e_TBxBf2IPJ^>($qk%$l-uPx<|jYx zC%@q*zwajxJ)74)wk#ZUGf>%NyU7h(Q0$aRpc_u z9s5^Ykp?^CeZ^Hp@Y38P|Wq^|G<= zoR8jcEtLAqd`KvH!xeXh``98nUSWHg zVrhS2JJVpi-|!2*X3-|XZvQ*mPxqdf%ln6-b{B)wyqAF8RMhUF^3GWAtGSEWeN^59 z*iFUk0U|f!*`&>b_J;;L@mdnA5dUSlZ+?MzXp@7NdHDT<96YBZj%y%aE%vG2aL#9C z?YoMPVLf2n_=fR^wHlGeI7)HH&4Qy9uR}g(c`Jj{>8alZ?>6|fK3`VWK6s1mou=-) zMSfKAV>r*|@)@^{f2c9ey%}d3Jej&MZ3x$ zg5~+_%613EBaqMV9Vk2Mp2#OEF8U{9XTDUnYZFf?ypNGj^tW1q8x`UCRoT9(cmvMw z85eVjykr%@O%*5A;_<6&cT~K)zQ_kD4tiYhOvPQB3C>i!tGVFgiYvzozN~mJTFLd_ zRXi_DaF8wSS&Q>hmPaU_SAqAFmF>EU+gBHPwBjf03QkfS-B9#9D8AN2`t!WvZPC0R zt!(#Iyd*#StBTzsKW z3x5rP{f&x8!C%$fRqP)WcYuGwAit&fL$lwhYFln;Pe>tdUsXH5;^JPMN4KeJcTn7; zvdBAoM1NmR!8Q5w5{QRhXwMLoZ>)H2WA0B?yS3sgO$9%xxVss@s`hh=54Mu>;}my> z)sT>?_D6~nFn%E+kJ_Ip?tt+N38`i;RQyI^ZclZ4i&xr{fbnE}Me(XKf@=qfyeP(# z<&nXH_nUaGZXZ_siwExywW)6B&oA&M-KOBp=nVGYxj;TMhey!KX>`Qw_Tnf9?zULGn`#J0;xMqn_vS zep8#e_UpOu&d7Rp=K?IxBFP7j*;I&e8g0h-XNNYlgA_Nx_{Z9f>~O_XOS8O@{h;Ek zN-S?|mowPuPh6EhjvVnEYqzx*DLz(>+~}rmt;x?l>^~JB#Cnz;+QSa4D*AOye(q_98tm-Hd)lQ`KJy^a zlr}x>ZU#I3eaWUrrF>n~7e+7H(+qaXziiJ}xs-p|zF@FZzPJ5cH7S4f5`OiyO>cWp zb;ju=?@RWv#~bX}>tkn#-0X*N{c9JhA?4$7zi~mEKK3BR&rBs+4P2(K*o*s^uW$O; zH5K<*A-I|1m5SRaKBM?K!M^$~8k|PSUZU?{zg#_IU*&!6K?bMMyU1T*)Yl%+P}-L| zk>lr8d$r=U_JXT5lJ=!;!~MlJui7b!?*&+Ny3OnMB*pJR?rJ;GzM%LM;K;T^?JA9> z{6gSHZAaLH6n9Hx`(tf-oaK`#bPeubw;gNOR@`_G(F<)S*rOCz0WMPFLwlZL7xK*( z&lG#N;&<>|#gMjB?XV_duOsY_Xgl35-c)eup}7Clc9xymOz>03@&0?;&+Ux{JNwno z?cD~aQT1S+-=Euu4Nf=LpI<`1TQrwXr&{YcJ~QkLgXR6$L7}tlJu1J14H2%d?QFs7 z|2g*ETzE~}Id<*lVsAFuf3)p9`?(ed8~wAuI|S3&pLl<;$S&Ga^yU5CMRtc)f~z3D z@J&K{cdX!wiu1S5vB&cIPcTlSSIz!okv$_t@HVVJzqMUtcWswfzo%gDDvZys?nU-8 z#p{9V2Q0SN3Z~#m98XK_qXti=dx)o)3d`(2M6Uf`Zilru_RaguciJwuTPf!EneVh+ zVW$c<`u*cq+9jSe`sUXJE&9r?r SS9XlS&il$=*;6}6f8>4Tuk0;~<$dMV_FcvD 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 z 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

4t6iOx^8vDf4(0;x6zp=_ZA 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

g{puy*N2br}{q2c0%3zECD8kb>D53;L( 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%0DGk{7x? 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 z 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

L^lp)jh;++t(13WpKCftNT5N`-qXlMxBXVooQ8g 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

=7I;2U05VN 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 z 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

)C8)uIAiD7AF`YH5j=~v*` zr%f*(3FdNPm@K>_z_^qi<#>P@vO#Ugn|_J}P775gkhm3ecNzPzfOZ&y5|$zu1_@qT z#=dSzjcD0(gF~g|i?*6RP-v-4 zrf;O&|KY^gW%NmT`DO0d1xn&eo-UtymZaFOXGt9Ux=*8BqAXVoa3n~EJFt080Wdjd z5{4($KT1+-#on8IB5s(Nz`Y`rR|lQ4=Q_=2#+#hcvEJ{gbM!g`&bf718iqML~ppsRyBN^a*`6sA4GNP>^MGsZH0(qp@IT_*ot0czK zMW`S%mVNR?(5M29JYm?xUMM2gF01MuPtK@b&bcjV7coC)%oP0Wp;wqv)N7sEg4(clkbqGuxK}9bZ;jeQp-MA+CJUB!Y)ffN zs6>`B=ZGk&BoS!GXpWvz7fl%2Cd?Rgoj+Q`pN>M2xZT<7YK&kssVg81iP;jA&q__F zuauJQqmQ+QTjm)Vd{8w!PPeco6PA%OO8PtGSDsXS6yzwmO06R0$C$hn44y4gJ(}WL zyPk+~Um;&Mn?8{I5|p9^JkZnVs6-*XtezG&35mI$>hp4KN~l@DtIzD8P1C11r14D| z-`$Bmp4;x5RG&Ob@IhPYdxA2=*6NRlOuN>qg0;n;$^+u+6MsjAZ+uyLI3lDCt1zQ0 zDgyWq#+F6#Ny8UW#Ing3LNoz`zdkV0sPKmcUcP>s3eVpF3eacqOgg|&5wt!Fa9U3XJoz6ytT5-(cMmIq(Pe!{8A1M#MTH4yuzf{^FVs=#s|ZvC=7-u>_(Sb5 zh?0*$v?Yj=9J`G#8Ij|--6H-O(KLm}ab*I#*07skVEM?8Kl<6IRH8H}EEn2aV0?Jk zQ(#|FfmsRh<`gQ1&6jW`IVpqy3qh627qYNuXiM_Iz0?S3-aUYD{F3B^$_l@fQXsnQ zzzs-PUn#S3&eeD;0jFmo{(aqE7Elkk55~?)dJ5{n&7p6a%`qs2BiK|f$SZyxQ_lkUlXWhOyU$?mtT%Qj^kL~O5L*2hpQr-P@6+KiiRfUf{ zQk^!1NIwstUx2>zryt4mAd;$G;Z)#UQZVV*O=J!09X>&RyZtcX`}=xK=p66!Rf` z(eM_q(O~lrO$g4N@*>x`tnX1;?a_99cZ&@_5Z|!dBGug~s_tCt#C7M<1R7cu7%Q&3 z0$b(OoySy>nWB~pQn zYI1-X#*vgg?qmOx^h(nEx+j*UpQQZMz>j%nTs2OV*s=B;ryqkU3wt0dlG_qIX)G8G zH_kBj(mbSTn51kn(+sM?8%7yf6lEpBRSMrk<8zoYuhy^9yDy*ErD*4m1=;w$PV)P^ zBPPRY1Q}d2oL)X25j+W!O9xyM439D6FH3&}k+P=1?r?)fEi7NjDi4djZ1iDXmPwJ+ zucN>ifeKN|BN`gr$5wxos^{eyuLeyBl8F6&MDCT$= z@sB4=`OadwT~GaKu~E!RyO$TY^e@{ybSN#e_25+Km@NxxN8>AGh#OqEak4kA1Iq){ zubTx8nTSIy)`Ju?9axtH)FLRtvS zeV(9B8!~>G-cDdqgd(jLprM%t@qKZIquJBbh_-+!#%KkGoJY~RBu=%VT!ezJXb!{V zgK;JThw6>sXUKIC_L?2t_!Ly)N#so11TIi1TQHSza`$Fquh!96+7MHb--(>xPeE7D z{)`x-Z(^F(BxKmoSKpv#DatyEgOylnprC2teJ`Tv)S-PIv93-Gr65R(x+6s;=6sr+ z8>Wx1a0Un1?PK@na1)r_k;vo{BI}pEwq{s(Q5>d^3cLnH!`Yf1VSqMG*3&dw%g+pImb^eRNcpViWC7ahN|HRYaRL{Nv ze~mSrIAGyW*uooZ1W7m)G%ak6mcp@RGh&1*n-R$(*}REVdiIy>zQyjZ;3l3ZlFeBp zT>Mua_8^^k`Aq2@_BTjfyNU^VHe}h2w*KkNIPCz1A3mviG1>Nb`d!$~_ z@rSDURo!*+ot%9A)rNfWZF_!8$LI-y;2UaV^aReb!7#KA%glslH_@WA?doOk3513= zGmD;M&^XxBswZXxzpPs9*(CE{~(LsCX}CIpjKiIRQS{t*k~^fI=(xg#}f86w5*$#Ygo5A7dSFS74Y7l zM=9Py?6I778B)x`rj4+;-GMa$?;xris|kpeDQu1T#ekT%s~c z1eYQOOXCPAHo@w|k_C2dpPp~iJ4&Uk6W;@j@WzM~ItHqRF=7{}NZ(zcEPX$KK_s6k z_CQvY!W5r^40gAZ?>EbAAMR5XH=%cz7&RirZA1%JNK@-PA zQ5(eX;1X8>Z3xOszcTGe{5}L;2>5XJ8iyuR?UFd@ICFj^^AM zmRe$pqkt##)ty@Xg{nbQd`ZD7#2nC)-0yJkCON**$X8P7bbK=*wZm3W*~U3~{~k0^ z;+ZIs^i_c?>072?i^WmUHOg(VxEdxz=8L!R3h#C30i_rtMuJl+Xc ze_q)2VTugujZ%yfv!PvtH%ep^(zi@pgBa3?W#V!0Cw1d~z$91KIoxNFlY|9eOUV}& zY1E}m@efC8Z#iIKPslFtz#S(PN zr`nKj3f!v($USIJ66ae2gA73ah5D3`$HbF>2yr*+f*{+(cg6F}&(RJ)Pb$b$Gwf7P ziC=T7A33R>67O)Tk3%;kT|vc`ry1qey?eJ8JH2?u%M9i4b; zCFTBX_zK2J!3s#o3*kE$=P?K8h45FASEZQJW#haQe%vqsc@rg~-%|0@@Y4)=UjhJm zCHzB%{D6v$-%{~f_!)+LT>=1kJ^UO)e&j&j48OpT2b@%IhhH>wgvvgUNZZ6a;g=Y4 zX|+V!CdQXZ$X3<&Z4*Z+$nhBT({G!YOc0d)`gJnZFzs}Oa}rvm#932zzzCyUOR)u) zej~MBKq&Vrv`YGI6BjB-8F~i^xm`idcXGc|LEeRoCC>c{aslkKggm1lakOj+c~L=@ zI5@vikjtUH66fy<@=de?3GtOn4kx0WN=UhaY=NzlkZM4vTnAy{={Hh4OyP7w-|4qa zG%3i{&;$u-Rgja=P9Vm6w3&{Bg(^u`j8}^Kz<59=-+ywgcyhFinQO|KI{!1Lr zr}HfEA4_V2o=2nSq^AkF(et~g;b)QmX~_RELlb`lKlH5NuuG>0eEk^s&q9{;e4&k= zns^BKKc4R5-3a^gGUC4j3s284!UoVYi2A1IPmh+*J+L}2PaiElk8-~3N&h-}j|0FP z(T% zE%3d0^S}T3zVn}RU58xsle<<`b#>LMRn@&Wz11Tr_e^G1M*k4CLdu)**jWPCdB926 zYL=`S8U3|-Cf9AWw4B$BlieD8+OdbIi%$6v)k5ku<5^C;#Pq`;(MzJIo%UHAIN;DD>{k&*{g-=&k&VaRtTeMWHm8bPPCEK z=PzrvDOb1{%57%3;>=Y2GFGcT4`EjD_#2+S$)|2_Y-}dzT5cd1Tf zjx(Q8lh5dH;vYtbJ(4FDvVIi20(~x@z#9IxDl~s}sC^3>90YC97MeQ^bYeEs|MiC5 zTmbGUzHfNKOK={ZXS+P2GU?Ts|M8dL(Co;+TE2Q^?7MD4H_9f=@GJ}VhMA!yKS6q| zJiapaKj%V^EQC&53|%e%M8w!XmanrJ?Pg-1XftZg0Z6~J4f=#Sg5?j$qcxMi<9O(v zRnU**@wiE^lnV9z(iI%Vc8tq`^wz1NeXhek_!_i=?Bz`PmgLv*-~`tH3HzabPNX-N zj|@%zQ|#}L`+;9|gHGf4SWm!}3qX?}fHo(GJOe*y4vm&|#MHZ=liunF zXbk%+%Tw@5<{y^}{E@U4f@~&w8f{;9TT7zsj(dYrGt^4FC4m! zabryst}kp1(nn5%j!6f-FJCe<*L#Bb+Oz==T?74t%S9~}1or#4Yj@+Oyczq@~7cE2>Fin zCPO<<(+~fsjr48QtaZQ_$@&*?-{jC(#*@?Rul`ezKJO58AJ@H#Q^8BlK`Up3mZ%7g z;=1spt1@IKiK zoAFyhog%mIhRr{qG1`UW`9myte{$qM$@(MdKb^P2{*3+DmHoPa^Cibl*c)zxt{n%R zJ_>sNXJ~TPcj+teSkBW?Prw=PL0@n^kLG^hL3^a%Ukfe21KNslcoN5B7{|3K{jM12 zO$Nnf+sw#BD8R$L6o5q*Hr^`aWp+5{^d@Oze>6+sc$a(vCDAE@VfPT*Y$TtxD zoPOrN51gO#V?X`+I(1Ze*uUVo>H^>dUeD$bjL$^Q?|Y44f5de^@F4g_F6aQZYeru1 zj(*UKJb&Jd;Mb?2W7|S4`r(8};B`r%`>DA&-?FhCoyoPIA>VhLC-*o$KNdv#uk@pA z?C+U}ksio>(gDtoEK87Ho$>oK>XV*G&ovnuxdwWU@!%u2|KqesKg;udN1k*Z>B;H; z&qsr|<%C9ZoS$X@Pb>`$m<>J0`JIpccaYaLi~e1Z?YYT$_ASp-!UyH^P*c*Ll01O@ z+MZg6ad>h*l-u<=H0`g@h~m(O^waC?$Gag&AG`;8ARfAA1av|Z=t<6}2=>d{14w^- z7y9NEbYN=emEq9c^ryuE;0e@3yw53}75@2I5!nAK4DG)Vnw}(c5c@SP{W&A&+cd80ix}_sbKE}U zICf=!6zGBaaz#M-9SA96Izh%FUS^;}-R%otC&|e0G zHwKcqUKU>s&Nv2Y(Jy!Tg7i4 zhnCI;^?MBchVh~S`>Q?mF8Aw6ZzJC^j?>)g;4LSie_n<*dH}7P399KIDR{o`>8HyX z$Fs4YYja<@lIu|~mQQ&H^(@{3O>~cX7#c$TZ6$csHt3V(&@`VyN8f}FV%*8a`_Y%H zklyMSXm#%6N2CHbq@ViHkF#(b=G27!Ipcl4THv8BW&fQokMzCCq0hKqI%b16^1Q?8 zx0fp-{RjG2Oipn0KIm-DtM68TpXG+uB{V!#l-Ip61=8ooLd{on%{sBY z9P`tU=KKJ*mNC5#bPLCGC+{1Q(x3gN!2T8MPtW$}Z;JHvlc1?t-zoad68iah#?{~~ z=%=c6k-yVs==fRC82WpLqu^%ip$8dv);$1^*$S=2^T$;HC+!aH$MNmKe#=E3a0K>+ z)NRz7n~+|D?ee2Gq2Ki$0Q)p@$V2d~ozRo?$Imi=2k^S9l>sLy3oVxu*FSG5xD);8 z+A;8YrpKoTf6n?EvRwN>mb(qDz8rewcj$Gtt9f7Wmw!M%jfeg<47!qfr33f_UjNHy z;87`|=jqRbFM-GMx{|yCAAAH&%Kq)&1RT%$tGULP?YS@%_Rnaq!TGV2{xP28G1BGl z^n)@ica6^FhJtT>231(N(nZCaQ(i=a7HsXA5 zK!3aP8`3LspLCOc&?Fa<_*xpd7ypRUtO8+62~(G<6BC`g*BCs z-#P=Wo)lWX4m5NT^q05LKG~tGSYOKk@UVN(V!W=7?611i^&Me<_yyET2kq^P>nq27 z$*50|{%Lt=cBUU64Svb3mT_Z2mmT7vCMLN#AhF>$o_rzqcrfU4$my2A#$I)Y5_AJ6yNg(ck8B|6Xeq>={!-hjxXI zp`hBM#(pR$mi@6T%;q|Zk6ZWcup_h36pSaH&#(5gf>kH;S%ZHk33(6(q_y6^YZij0s0DpZF2Qzv%sBEX>j|ENd^Z|GXR@B| zUxQ=W-!J$%N$g&vcbN>WNi9Q7Q5xy#xIW~hf9HCI^vaA^H7bEC(El#i1k1nRaQyaG z9_Yd;(7NRDHyu%=iiS~fpgA=-ekNgz<3?X zei^`iYmo@~YJLx$;0rzZ7jzfb=lTP{!v{hW4~7=|6&hRuI`t!HyPMFKIiS~h-N)IE z+q_O+wm+8Rk)<2*FW~ytp7H9_-bf$DI5nGo`jqX<$axh>{g%(KE3q9DE1`VzYS3p@ zp^wPH0pN!8^TIj7Q-?#J_J^jW|0JS6R6mdOtFGr`7r|Xv{{C)o@K)#^?u&kY1wMTe zT73)jqs-9J^y4Yq|D0g{tDGO_yCPq}Sm^!f&^|?>KM#ZMYYM%``fAWm!pbB4Z4~q; zuGe*Vy@73!?n^)U^p z(*|sSoW*|9Mppyf7Vf-dgA=YN46n!dxFOvV7~hNKeJ*3ynI1YZZY$rQZ%_e7*fS z(%b$BT{Z-|nC&Q;6g-fA9LssPo?40JzGJyfWs!eXe&{`FU#=hbKScT-_U9{}|5X*F z_hJ5Gg}_maJBOJ*FE!G)EPor1=KBXiyFdo|1A3CxhG+h|9 zIQ7q8!Cx1Ie#7_?QxW_N=f%3~;C-yW!A|gY`u70F>!)v!Uc54N1h4y1C2+z9XnxMC zh;iU;^n#jpTXnvOTZ9N51y!p!+$_zWu>NRzdf)ffk|Px84D^T0whLpB4a@`vV%v_Kjfq zll1@mv9O}P4fP4*{XNF1=c`fQ*s;*ex1hiEg+9Fv&6ORR zj^{bT{%SQE>08c0FVpWn;&|txpKXeOJ!BR%3;XfTQ*h1N(2swIHsLrNVE;^{E~oaH>-pw8a4oI_ zP3hl99wPk%=3mWvb~BEi<^9HIV~}qGwQ((Q&ic>`UqWr3Z#A#C4!IcXz1bM~5)YD`yzm6{%g4~Ii=b>vr`3#G1zG=i_HRypJ~M>v?O6ilGTepU z41^X>4J}m-T8rz_nW|uUN0j3y>)8*v7zYA5zVY9~ewF>Fc)!?{-*f27e)@>{lQ7?X z#^dB%r!F2qJ+sNX4}qn&jNPj*)4;;ely)YMic`@|Rbm*~Q=qBpI;^1EVTrNNN)0wIH z*q`4sU-?(aKb7m$iE`kw9QTH-=h+0Lmu5ZbGJr=h&iDBo{EYonsXe$4H52WxIj(_~ zU=LgaEfNXM7zJ&f8@hTpG^iOggzY-Q{OhP=x4?da@%?l-IF|Dz6XW0S+*gG0KH?4Y z=izy7r9`blXW z=K!y_a~be`#<__chs^Z5-SnS+JkNvY$p67_(0N&*xj%&_{vNuJ^`Bz;vKmMauLV6w z9?y84iE$^6@vrAc$TyDdTf(@LPzveY3qg;}hOXm&W&Ip*Y3>*1w*hy31D(z5FG0WA zwifACM?%+)ftKcVPGI|o@Ol=o9|P%Uzi^+tbQa4oUtl8eFGZj~uZQj`3~j*ntw;qf z!~VaAM^0pV1GSW(EiI`nGeQIF!sl)a=%?L#A-w|Q_Tc*9 zo#&w&IR546@AH{&Tw~a)GEP-b4=&abnvv_`i$B2SUqd?vK_8}pUat$i%X+?H{LMTB z>9sjdH(P-xFrP2S{nsf-AKet1o&J=U^?aHS={;EAv2ozF9Is`Uz|GmdeAL)0NH526 z%*6FAz9G^#Hh|8gZkq>QvIKg9@qZKdZ8^3vom__eGxM#dE*Xk^pL3i78PDF1K>ENR zphvPp(=a~yi~`T&^T)q(f;&HhTCC?$OYo$E(1O>YXHP-fT!n^jhprk6J+lrv`5`pT zMQA3DOFw?@@}wrxBW^$oGv1`9E@!{)VZSF&iF|pfi>T#3M|x52+f$VSr{j3mV}Fm_ zk94&ZI(#!U_+w~Z`o~~iPXqen_SLZOWq-xP%dbB&aWN^!EJf{-KT?3a31zd0!~qk<@}(_0-;4X&e@r6 zQ43UpecDRsrTI|vLz(72E;Fy^Vju7x&Z~0y!H4*H-D{3}s^5`*?l$yO?hlGlU%x>5 z2lU^;9GCm--yIx}s!YGa@pyU-<%->dHes9}RUKTL{ZWnM70UjPXSq_nk=)}!9I|FmyzSxh3$CAcANJ%n|aqfG4eO) z03G!KbOQZ6J_J0B=NsK0oNyU>>JYTtEogiKv=MnD>yP>Y={DExR~%Z5-^nqSo4V_25@&de|8uaT%(DR&68GXT9IDWsE1SiS~H9szA`k@H*r%m8T z*`Sj-f5sFCAL8{S;k@wWzTlY;>;u@|;a`D2VShK~{GCt}>32s#M@@y+;=E~09$W|M zYaT-9PlD!VJdENz_-zK#hcF&yT?B403)+4rbopZFUbZVuQt&hK*RQ~xeub{(`FHIA z*JnH|!v3f?6zT0_p(##5Umk%Lr=A@T-pFyj9s?dsoiPZUc{?;K=Y3yyE1jf~`Y;c8?(3D(fhK~bR`2aeU@%HHz@XZ8h zqtwvX$Dk|d&wHpBsAU-^PVzoGka4yR+o~(z5Bu6@I1MnfEV-pi7JTy^vmhB6KvLKgV|mzr7C4!*))k)@Y3M0pu3+yZr3;c;08<;CxBM zxN)g7%I#kVE!qW|i|5-!|9Q-O5%kj$+mP=H$K?$*Y$eifG=qjuf*#~N|BK_%D<9I! zw}C$9{ZoziW=D2j=^MExR?@taQ z-!b;j*NiJwI4>LVx}N$Y-^)bMA9$V;nZUVcJWp^Zq#t1YO?cklh9i9@ z$9LNuaQjry!R&{F?5}*>7q( zcMHJ7=)VuSKV7*H=|3NZHW~xn(Hok;{;I;~)y=4d7@x|JKcZgv9ObU8fZpB*y)XmX zjPtWq32+T^ea6|*?8j-;zgRvQ?WMRMpTqed%Kn}fhx(o{e*(|-wYxIA|pFCHv)8N$|3dp$piaPW`~IazM9pT-u!km*D3|=a{cv6{P2?2JOrAY&pS` zcpcMgfiG=<=FWz8ucCj>rJqb<`#-0~GJP)Vi{<>uG8pxY=m`CaasA;XaLMz~UL5CA ziNTY{L%XuvA==yW`S1O?un#2Hnh$PAotqR~h4p2p=ApeTbs_6r&g-7fcwdh5E1)*& z$ublg%=CTCubIB;IqU^$LJyHYC9iyj^vLSaazmguSWgS4=i&KAvwRYkAMq{nuc9`f zUZfVGzNJ>rh5TKp1?e~GIUmj~hkXa*__2%Nk&Iu*xW9}piu9DZq08y#hp548k^Yu` zU5E27;t=(LC{0fpg(dx{P-B`cL@3?$MYA)wcXT?{CpyMAo4x_1p0*ie>*RD58Jhg z?F{`3>1W6}d7e85kv^W9WFhz&*R_T$S0n`K)=lVd<)9O?Ls#5}KKv9~hJI1!C3qy` zO!QUojtkHk8K9kMANvNptS|Hh$Lj%~OEjQAotzB2?|f)Mu1jy(p5$L3eNa7Ud1^|2 zPjp9Dr1$Oytr-pdiSvInHJUzoa^*O$fz>-JHW|B%<2o$VjP_7~+m*Po-l+`XU{_2Opm zhj*ZB8D}mqZq|=N`V8*(l1&Gf<#^}l3SQ0h+T8y=+l%zjUC_6TQ(4G4%OL$6*P|i4 z&sooU|L6ky9roJ`j(7QIex?e8nzQNF(4WK2f zK&O_14t3V2luQ#j3QD3w&`9f5yBJ#dP(fCNMu9TB^7}j?BLv0Yf zUd~UouP^VM^io|@tB1k~(hli2HN~mlPen#+^;ptfgkU@kK|V}LhB7HCGpJY5ZPZD&2$Fo@#CRO8$iV&-9o$CN)m5IS7qhvDHt;BxDr2gPqq z`UPs@KH!Zjq3PLP)ed|{_5~*Y?DEhRiKz3Sk+R=5_O`NrG`i#_((CtwUXeI$(rfmG z_RfiXQzed>^wr|WMsKq{g=Bp;e3SkDE$#Is{+jgIPOx9;51rEzI*a*I@OqyYL;8AI zFHE^@vEW4Yps%Mw3#Nj-io`dQ@5C6SXJx-6cdpap7hlRco>cOMu7%zi4eh%P8uP@c zCx3DA-;|PnsQ7(Kuk(J#i%NeR zy~ljbq<;;+{t4Ru7plY~lfK^{x=Qw+hNESCjrxf{8=XB2YTbc`JMAc>MwT$|$My7Q zVTmgrNPhF121fU_hR&d7C=Xs)6dFLCR|LGB<+_A}drEsuJuRt|tAlHP0yRG&WbC_G zPe$rS*7vX&>|aQn{2s#Hqezb$cPZatH(Rf~_c3Ko0MiqOW?$zkTbxPHo9 zk5ThW2uAx!JB${insynkPW{eNt$vcYCH1=B?_F)U{F0BfchA?*-2u?2r=Uf|zm2^~ ze(1$y(BmVZ<7B=X`_B>&jLw(wHkwcl{2kM~F<(!XTflmHOMEc->t#o|=!)P-8Gn{;Yh#Rh^8F=oHNT|4{#`kmDh?VTes0p0!=C)Dv&yg6 zNO~^WXBlm}2Kq{_)9~Ie!Qa(~_Lul#(r1UjzDfGa@N-#XjixOCe%1qeBow-}IJDSO zXnW~DlmFogXl1#-GJHekr_qDdh0>3PtIPUew5jx)(WMd}jaJSLJuCMIg}wTr{(hxO zh`sk>==aB1kide4zYJ^e7zN2#)sE|=@*S7d3P!}+KksS@^_bg9l!3FnSVa~(}b zZ9xsAhEQcWb@Dx*53Ng1Lj9BJ?M@rJr=7jhC>0^?^s%8C<$l}nA2QF3CX#j=y-w{X z`!B=CpF_aysaYnE=qkyyGs2=2U06f0dJJ^nDn(yK2N(AOFL^w z`X1_0>N)Br>bG*;HN4I{N&2_0lp7>=qvAG>wvmA}yd^nw(FW+3i=a*F8}(CNC4QPX zIk*xuiNxO{vR}L={cqxFh{Q{yHzcMS%~D9-$1eG+%f2;2bZi~)H?p5J=_zQhE%DW` zX8C{jr8Ro4SNLUD{*p=Jz3t4v_V; zos6ehUyUZ^c&356+ubj@mS5egH>?A-ru47jC`Ud0Zr05nl5W;bqh{SSYSvAoX5BPu z)=i^k-85>}O`~SrG-}pOqlHF6vr|uHmfx$9^dN}`MsH^U`z-=@pnU-K$}OZPlKJA) z%kwPZd8$jC?&0O9jisOaO1iChouU^J!nW_=28 zmg?li*|hvNufqueU&`*Olk!y~$$e#)H7L|qT_InWyU#eW-*k9{|7&wMD*TZ48TpLe zPyOcbDF5ZcLBi=nl>8ax|8-^g)dQVK1(SCO4+~AC@;IEJ3(EQBHycz*hvOX%5^jL} z&h=%}0jj0LBm5IdZ}iuRRR?k^;atK!z4B|7L=E)fK;aQy+>gJosMNt^EhPHwuw>|+ zKL?YIF8(gZ2?xsW%DL>%WH!XfZ-lwHON3UVLOncCc&r!4%dUEd73j`?N;pvdbCu68 zv*@)>>EoqdusrYbnO3H}$9_}T*DL?m5~W{j zS&E%M2a|m(?5jNdjd5`M zPv-!B~J#TjMwn-lth+n!T+p%>RHq}B4!Om2I7;jg_o?~4Dp;UMMVouy3sbGYqCgoC{Ji9B_RQ#sxC(owSe@b_>X zi67qFQg~OW$KJ__M<2S%`+hI0mR25CGT*)Vi^lJAW@(SNy+ zV?P|~;Z(A#d9BSJ+x$7`JI9&-9@f?5NuHmFdq$f0AWNO|=b-QW8s_{S&MMdM&7aA9 zI2G!#8$RS$&|Thepeo|#^-jDg>gMfE{l(n;Qu_O1sE6Cg`F4etaM_c^=a)O*(2{OG z=Wr=EXZcuur75(un+rNz#?7atf1ZVwb@O$H%emM$k8}N@Znh-8eI4rI0@7a>tT4CT z@HH#k%^h-Um0(4_!@pWl@9>|Y<=tFF^1rewzQc*bJbX=7=eJfRx7~2+u&QqMm-0#N z8t-uSu-a-o)))T<5?^;%_0@E8OW{EICBu2-Mpccyfm%lHC>*F7sCDF6*%j`PSF3I# z&lC<+4b@(-vmS1fUmrg2@Ii(CYoZc4QxN-sf?-WmTCo2H*{v53YpSw19OJ)7xS+6k zo4RZNP$sOID(;m(BCNRz^|DtGuI6R08rDKJ@UqJ@7j?^v1BG9E@f5$7D$of5XkX2+ z)~c!(HwkO6E;$^d(uNy<@1Xpg5HVRDl)IAFVI5R=!z#$z<#oI;)Lj>=(MI-DK<+x~Kqo zI-pcS0QL)ARUR_-3td$!GWHAI)NnHP3*FQ%hY$K=ztCNMOU8boyE;O~exbWMO~!tq zyL#ntLICy)J(Q0V@)82DU+AG`k+EOssg{tjU+Aem%!~H#clHatlzDrfQZcHL#Gg~4 zy;QsxAM)#~@_z*TTk-!mH9(d0;(=iU)J%sDD#Yi3>M-qTr2f63pR24+h>cN&9Ui8l z9X_ZqpGT-^v{w=P(6BF6`j2rwAK`If$!f2|2mP^snyhY-v45JZ9+I(tnyk_kcFG4} z|1?EqB4htFMJ*v?|1?$QbC!gI3hT*KwcX(u)kgY%O4wA@&{-m4R2q4@YI;1@;e!h6 zO`KXndkfj$oeG_)f}ABIM)h@gj>_loL1p4&_OSUXmiEbF9~!n;4U?y4X1qTY&aYRf zTMj2^tY0hD9B0V^2ddR7gR|r$XslmrRDLqnueGW*H~{;>wQ2+z`@ywpF&X>8wQ4^Z z`@yy9CK>y|waRvuw1fcc2iK|`Wb6mmsc16xgX>gVGWLV(RV*3%!S(728T-Kv>J1tD z!41+67(eU>H>z%A><2fhNo4E?H>vex><2fg(`4)iH>)RP><2fiRL&Bd5P<#Q7FC#x z{ooc=n~eS7R@Iw~{oq!0h>ZQ@ zN)1-n-|UuOPIU4s>~FqRIUJ5rQH_kh?~zYhjXfa%`<*>%JsJC*z4D2vvBzs$_BXS_ z_9=g7i;@t4{m*_CPR9NxUiEi4Ue}fRv@q;|`ige!hYrac!uhZtI;{MiEl#{n{E=2G z!j7oi4#%i_vfr8&c1#T>mzVP|3_GDtk)KLDS`qfW$`Nkri&3eYo9j8PrjZ*qlV5h! z!coSa5P<#h4{DynG5!N(zZ|4~P^%q|SLS{^NS#rGqfLI-{^G1spBTQOQ#kvvv&wwS zz{S_Bb1F5tlCvMXAiqWkyWwB0i|RviYT;MbW%V)nTW7y@S(PH+6;5jZsG=Q?^PeZ{ z|GKd2>MQb}vR^X%(BaAHOUv_*&B8Aoc0CWdu5wf~=LZL>>#BzrZx6enMmij$ddT{+ zJ?y6PakhLhYE6AvKf-RS#tx72&s|vVH^LHBf7<7X{d?j2u&bX6$uHa6choC#b?NW( zVRuzPW#|0L@U^h}Dx<@$=N*q!CD{G*%Xr7BN2($7&z4X91BF|Wmpc3RU(|T=Sz+l> zwZY*8omQ^@ZrHEtq{BD#@8WL-#D3A?81;wrU!ZWeDxUt3&-K-KFWw&ZSgmmQpej^B zej!vnRVAG*FXDfodamY>5szQU&zPI~4m#KOBnKrS2rPlDV$1pj>A(`1{t3W;ePtD!>;j8q)lydem=kPF!p~Q#{SRE zzGi=R%Gcjj-q-BUg2UaM4C{f1u^xCB>p_64d@`&D9!9)R?6xD`dl>6Q61N@eg@+OE zle+DQ_a4T2k<4w!df{Q*e9>#j_VXXHa#(L>ttd}0fdKu(CKh{eR zW4%o4wqw2YFzyf1x$L~3_b~P^9>)EMhktUOS9lorCmzQAhKHNW{rrdF9*&TBkUu-@=+dCR9-6%7w@b3ch^$9+5; zCGGXL=aTzlFS~DFiMR5+$yMIBM+w8(+@3ob0*8t3l?cN*NEL+P%ZRd5V~L z>9+eG%Ka_}$@a^W5hmgi}5hwduxYxc)`L*1MV^Fc0(=QHuNm-g_P#-@Bx{RZ(=Rd1tJ_3)xP zZB0`kPA!*EWH_6{uJy90F6hOz#6BMEkNeePdO8{RtHtythhzMc$at6ZDXy=BRlVx+ zSwwhoJ*~DmUyQom+O(&HZcxYYDCd5yS$GLuurAo#KeZ1pqu-Ft{ZiNPvU*8<*!$(u zs$Y1R-c2_5PeZ~Z^zjD99;1rOc#I5>)GHbyzq$S(6|G|%!M7pdmdIb&@$cHYY%B0H z$3N@p2INd~Ke#uvuD%WqxF-BsH_*9TBmYwwzh1(F$;+Jb4Rw+>#t#22o~WTt@5R%@ z8|!s#9lQVRX!H42Grf(xm%P{E81=2}&ocNn)7NPqBs?d)g--t&%8zllrLN|%YyZ|t zH*whYc~vXz)6UZ#d19s0cyX=p);iu{@mJZu%I}lw-R({O7_~|K$?$83UC*=HX#Wnz zo)CcjOdFkpjQva-9Szpl&$QJm$k@-c)pwa6`nRY%l^{Y+b3kc|DzXSxv?`!_A~9ZeXPujj{Y)2Ki;VqD7u}tV{Y+Oqi;VqDSG|pl z{Y*E#lZ^dLH@%yT{Y-bghm8G9cfF5{_}@dvlM%mr=)+{}XL{)8Wb9{p=-@869_(j& z=&EGwXL{)VWb9{p=tX4gXL{(pWb9{p>icBuXL@S?uBd;wEFJ4Zd+EYt>}PuE8f5He zdh3B;jptRpbsQP{ncmv`YZCK(4Eveh`WzYincn&d8T*;uI$1a5$9|@_EK3eY{3LY%$)4K36`a1dR zHuCvJ_&A+mn6W1WApVTkA!Nj#@wzz~@n?c=Pe%Nipm&lHe;$%$cR5P^p|ABpRe>JGUCry z`T-g7XQmDsiTV(KX6i69;?GRon~eA~Q?Dc={>;?Z$cR5Pb?PrrAL7qU{Q(*AXQr+} zM*Nwj8;}uyX6f-{#Gl#vD>CBGZ2g>!_%lc69_5^m@n?>1Mn?RZqom|bmiwz9HAi10uXK2>Hox91{>0CN=IM*$z=%)tbm8$}#HV?>EE(}>f$jlj zJXxeCIUJ*&%kvAZ7U|FlrhJTQ39>ycBP z`us89?&wuu1%JJxuanD4f8-Cpqqk2p`CaSVJw0SP%42=Hr(+$CQ4^f?>z)pdL;my5 z`gTv(nGN14{xmD%p1w#vDm*yifv!3i_F!2bmqk3(FXw^t39pTKq;}KOYOhhly~jNU+N(a zV>~ZJywZ!o>Q{NbE&pywA11#cKLYz_m*)C5vfQuO&|>hdTb5 z+FC(=Blo}Z{Y7gRd78ve!zampvR)d#L0%sLenIXQ4Gvg?>p9Q*GL!S>hrJ-V2|0@V zeLmRhk^dlfAm1c^P7Y%E$>g5orR0g^-Q;lc8S*;veR31|K90G*KgdDk#A|Uq3FM4q zUvge@PI75-F>+<{baEr|pXAP9HB$P!myDm!I_G@ql<>=lG?w{w6qzropp4gF5kZz( z4}L8D+e@dlu5L8!@|SehLx)GHKb-wddMoQDC%=DYXFSte`N`GgeonrxX_X;g3_nXBEy6 z8RF(Yiy8jV&Bf*W#JM9saD*@DOlZZ_-Lg2;+){;sOT z-^j{t9xCI%F|vxAO@DqJS=G&^zrT*G=H_CR<@>hPbMtTV{N_ny12?yo_W$n1L#2QIifrh% zACU1&6xGPh>xEN9HF0x^Lh`R2qnf%ow{VuI=5Br_{*gASrJJt{XNhX<=1Jo3xuV*- zd8lx~sCI5HDD5d7)xph~g`=W6xw*5f2i2mwxVfHi-KcJEUf{H^hnr)a_Vse}Dwz+} zqWZXbqHx`)es10^{gpOqfSXqcXNel*=9`ZFb2p!I>_gprxrkQHqGH^9P`F*x2siu6 z`1FYS!p+%bJbFZpa&t*h(~Uv;C#ySbjs z-?~u~-5e$3Q8#L`n_7!d}CjB!vYNeYymodEB&8KUa`qsJmvBT>f9;T)SoB6aXYJ;0idslg} zY45tIjn-LbKDwS4ZL)rL_@Muqk4^mDWW91YUgeN@XgJXpGk+5T&>x$uJa)-&<=L8S_us=B>!4KQRAn`xF`T+t0p5#{BlTesgmzpiKMNBl@?PjlEcACuX}YUZ<W8^HuEQ0bPhM0`ExSrLpPiGb3Q7!o6Y?3jn3<4Gk=mr z=X0}}KPRI;cC(p3=c5X`+037lQH9-X=Fj=4qHZ?xCv9|bH=FsBHM*pm&HTv|UE0lN z{^W@+<7P8|l0}zwvzb46qC?$m=1-pJFgKg|lPo&i&1U{2i;i@&nLo**qup%gPqOF= zZZ_+oZ*)aBoB5M0y0V+i{PB&h>Si;4l0{c{vzb4>(KXy`=1;QdT5dM;XHQfeH=FrW zFuJar&HTv|-N4Oe{*;Su=w>s2%0)MEvzb2?qMN(f%%42bE!}M9PoC&jZZ`8LPjqX0 zj@SI~Rc-9EUR*1>tsT6}%oxm{B)*^7BfYp`bbH&{?b!Vn$oXFDPIh9lE(p#>Hs2>T z_Ppd}g~4UW8|C?rv6ly{ByF^+e?#HoxLy?xzMj zpZ|BU`;mvq`FvGZdkwjgJP-6$-R#R`e4nGceIM-KPR?K5r@Q@>oJ{uP!-NCB#r38Y zZWZ0bE&x`u<$BBe_p+;caSxx~cAhzSD(W7m(A6!HFYhm;_J2>9MJEEuCyAOEyZ1i0Fu^0amy~JL9$Yb{_ zzs7EV#4!A+c%rTLIfoMh(7)U5z@x^V5P<&OVOJ%$b^L3WU59*H#wSkgvg?EO9+@Ax z^e%fEIjKBv(rUN8fPA&SRww*-+i%FGf0FoqV-G*(oX`1w=QjU6_85m<-|yUGPouq* z#KVC;d+e9wNa>GO(R=M-$5FnnJg+(7|DC;r9MRbL*M8gQgt3oOJsf|Ex5t2;=RdwG z-d+OsUnBLs)(7m>Ak>VSQf<*&X#ME zuIlgzm0Qc(Jf!^kckGAk?X*jHmHH3a7v8g9r~R?mvy?w%H~Zc>KkGkiw|97idC8X4 zf7m|$j{S&zmUbCF8IL3Ov-j+8X!n<$b?)*QlcJ>Fs0e)^cb z$zj)i`k1}jVb}LpkJ(4O%CGi6W?ytTA>fTXf9S7|+t(eA@%NYK?YV^SIea7FN3nNO zC+wHVuba0v{&vDHf7-M!L8HG<*wx7eWxWy)koYQBDY3?*mbzF4oVGNywD^j#zpwQo z|8>5jUc@dx=_kMqFQu7H1-H?Dkt zr~kU&{@%|T%Um|?`A^IJXZinW+kxjKm2Fy`tQ2#|GDe? zyX$=ay8pZRe>HCWA$t!qZ(7Tq!;IJa?f6gauKWeVQQtqY%U@jQb*(G^J|4;arW40p zap^zzgLAS+G1m*V-1GT;cFprhu9L3#0kz!gr;8gn@!EFx=WE&9oAbP{@1FTc)OLARXX^j=c3*{d`MKNmPx)QzxNBS%v%g&Ye@f%!yVs^4 zYS+MZyW)jwJpa>nnKzr7{&Q)4uG?Tsx!83c%q!3O?<)7N?LI1-(_UBo|6HHzeDAL- zcX_=~@jHiY?nivw?SDU?i9eR?S6u$*D(^ba`}r?4K!03iKe*zT>pETPD*u1FpWgR} zciC1)<+}oAd|h$q@8X;*?!4dc|EWLycjbTgIy~{s<>#*JamCs9&+D@P-Sz&b_P^_& zY0kb&tEJq3O=ySy2LIRfEETlKM($H-n!CV^AYX+KR3_b zZ}@R)QUtB-l&vzyd@`cE=JagaUozGYdoB5K> znP)!gk+Uzv`HVlkp?{cq4L5(!-kg5qI&TLrcJd2rso(p&CjD#PpPF)}W^Xq32eQ7K zc6EC1d?ww*1MhU>*R$)HbWcBh8X!nEHF>iu{qNOM+noDpPkl9IemT!m9Mx(D?NCd7 z!{-g}v&#?ORZBH;##5{J(_Q!XCf)RdH=FBp`JKz|&BpHXLzmr~josz1F1t4yd*x5i zk1o468~Z>f?xNkMeco*BL&D!J@6E>ET|UDy{qCyYn~i<`dv=(V_ zg=vpBoAWN={Bx=6ydk_Fc=x(2Ro6+k-S-3U+wq*pwJzg2O*{T|y377w+u@39@7C|i z|Nr~9=@&De|G$5~JAdByZ}`uDx8GXoU(XMhfBsj0{+E6n{QpuvmUi{^j??e4na|#F z+4!M1E6ML2m#z2Ml=t@A|8YN-^Z(m8E%tZfwD3D|TKJtfE&NWLHu=5d^m}aj!#ggU z{_tjF_x4+3_h#4nVEXN!;_tinMJ|7Q|9;7n{!e~>v0!+xzExzkXb&=XrpuJ+Awb_w&!=bNGL%_n+(sxc`7!y#JK% z{W|HM&&6>6D({!#b2{Aj*b2{;R5z#HzPz4)Zhv9+1Fm0QdgNTc z53k=>p?^H};QIa9E>Az4aQx9ym*|&&=RbwGFLSBOPt(&cUFj~rbFoWZ^5s8*M#12ozZ<9u7A zfU0DHYdR`DLG9pZRW(|hPx(8knJf0+G z?i;m__9NB?c@lL*9kDEF?<}=}=RZyRB}cche4vVO>Isz5#d!-k{6e@_dHGaCb#~IT z>COsoZ7-yMDPrCRiTv4gZ^!;Z4R$m_k96}0i5v}dfV@S--07ZHfd~SV3|}FkJMWc(8fj9NGg7Q~|oOT+ebn&Z%GC_sRBwvuSOc{4XS*(W81A z+Yz8mJ1$E*a>^Z+{JRB}PVShW>XjD0MFxH=Z2I5V3b1R+Oi5zhP}}4QK^hDFFYh~J zeQBtfs2S`8xvoN12HSjVr;wFYng03IN<+;=4PpE8*jXh4EU{*)b>bg8teKYi_D3B1 zCr)X;O@F0TQ_@daTAObdU7%hQ#oLpizY62+$Xc7Xq`no#+m0JL{gBpfsDG3v2U+Y0 z8{cAzu+6ud^4fXyM7h2~V!td;(#qO3ZM+q)p*C;7t0d`0Yl-6RZw>j@+?g8vwvcZz zo}=(>shQf;-$s5Q7vnZZnYY|^v{zb@((f_$Ol`ik7Ap#SOGg96f6aByvR5m7i>9Sr zx1GEl(e7Yhk|#Xt?9u$T%u36o@3&hz?K)`pu=7g24<%oW_~|pzytSa|<$4U8@pL#x@a@W(c13vtcgkm`y-Ln=UefU`%y@-wJI1@O z5A~i_X(jGll6?5KWxO)qBJ3fI>xk#|-EhV^NS)Swa?AYljd%Jn3Ds9!a^gt_-_v@P z%m;byWZ_>oY;fjFpm1xMkH>v>koQrKa6a90>TlO z?UY+eFCl(coq14P6yFj%u0EFX)%@~UHD$*o@xf{!{n647el$$f%%51lC-%?c z&lCI*&!_mkVm}qK&D+M!+v^b@i&E$CTZAi}IJ-ycG4?}J&oQT-C-#c7O8xAoea6W7 zK1@`|S}l6s=ay5xc%oYh@#Gfg|1H*+kMXW3^@&|r{B%pA2+R0IFP+CZ&zVGdEOY01 zUi{6>hf9f`*mGqbT}w36ZX(w^LE6zz+GG6DtZz>fJ?C#~K3Din&FB10&FA#9=k&wp z9M|Xc>y~yFDWBQ@IsN!K{Wyb985zI4l8*U=dI}1|@1JuXKj*x9?##dPV#hpuPJh|V z{F`}x#5Z5HndLXL9h=#%&1^>x%fyAD{%`mjpl|papl|rwpl|3OZ+M*x75b^A{b#iC ztE7$}ua@#A&TbRMw@TkAd|Pxh*U!<;`l8in3-e*L6)*XZ__tAYWj#G1x;5eu>M_+ zIdtB{{;G-YB*!#XcR7x^ny-$QAw8`uuW}co%C?$80}aqI@YR=wT5l_t&VMatbuKMtch)Utc7i^ zw2eJgYHn|TCCAS8Tsd~P7s;`=y`QW7VPBKuYWt=f*V}jHxY>Rr$L;o0IqtT9m*ZaB z^liNTM*jb>t&{kxy`}uzV{5-zx<2W&A+i;^A{(F z+NUv7`#ff8M=)Fa8s=%=!~*TRSfri6Qtc<`(td#z+HbH@JB!uYpRh*z8`f(7#5%3U zhw|5YV!hTE8?=GgsI_2|HUgWqG1#I_z*emj+qCJ}u00q#w1;D-HXFON`Pi*J8hf-Q z*sFD6pLR3$Yfr!d?N%Jr*5Z)%G#u9M#u4q=II3;LG3|x8M|&xbYp=u!Z5vK%ug59v z%{Z;S9cQ%NIIF!E=d}OAdF{ivpdCO-_Y`V$&!Iv05*l@*XwtobX5Bkz(S3kc-6Yy{ zpQBy(H9BwGXz7k~x25G>MJu~Zk0E?qoU=vH8* zE)A=7tFT6Q7}n~J!a7|Z?$Q-vy{;G=bmiEntH37R@z|_85nFU8W2>$X+jOU6yY4LP z(4C8&x+d(>U4q@ZE3ij*HTLS-u}^ms_UmrL0o`3VsO!NY-TgSMdk9B#kKw3p5XW@S z;vU_LIIepYCv;;tse2ozbnoM|?qi(MP2sHWE1c7Phx580aX~kalKv0W=+%Cdzg~|< zy%9}%e>CfZ(V`DWtKNn-eH_~LN$Ak0qEnxNY5GGkLw_V@>T@wmUx3;A^_ZtG!vg&# zEYeqEseTK(^e173{#2~g@4{;RnOLL$8`kPCz&d?1?$Te5_4=!@L4O@K>N~JWe=9cY z@5C1UJ=m)6#Wwwe*sgyRJM>Rtr+x^#^e^68|kHHUn z4QA{!gkrxT5(f;iIB0O-kRcg|4J&cPa0reXvT)3hgL@2XaNMvCCk&-HY1n{MhGTKs zP=zyw8k{w3$2r3eoHv|-3x)=iJkLXo=S67nybO(=t!VPR2F;!~pvChRw0d@->-w zd;Sw0o)4hYvmetuAIA*Or!mv>dCc-0!EDdhFwgT%Ebx36i##W=)bkT`d47Qvp5I`l z=PXux{)9E2zhSNCpIGOq384HvJ+a=?7aKeSvC-3lO`Z|h>=}bCo(b6M>BKhAbZqxL z7&|-<$4<{|?DEXVZqK8!$Fl@`Jzd!6xf%OCPrw1stvKjei$k8L;jrg!9PvCGM?D*H z%=1Fr<9R8LdtQkXo^3ekc|A^f-i*_px8sawH_m$Ai*ug;!g)*RPo2^;msdPic&)%nuQaUoT7@-UhheSPQCR1dhr7HAvEHi~8@$S~ z(W?TRypG3auM@Gw>tt;8s>3#~)3M#_EbQ<)7dyS0u*>Tb?Do0>d%Uj3UaxlS^STN9 zy>7z+ue)&2s|Sa??#E%Thj7H}F&y<8#4)dDagW!FIPUc-PI!&sq}SUx<@G*Jdwq;E zUQ;;h^%c%}eTVa2KjMPdJWAewpvGHmru@D2X!JIs$=e^z-oa?`4o9oE4Q<|WX!lM+ zhj%JEy)!V)`%ujAJ`yv%b1}=i0JFWz7I<&MBJWBp_1=Om?~|~?`&6v--i6iP zXJU=_->}yE0<80H#$Dc*W4-rP*x-E~HhOnpllQII?0qM;c;ADq-o4o7{UEk`KZ+gR zPhzL{5O#ULfZg7&V2}6f*z3Iq`@G-7e(#TP!22^C^q$5c?{9I~`#(71{R@tIFW{KB zDv0v;*5SCfH%@q)aMC*nr@X^(+B*toyzMyaorrVZDLC(a5H5IUB0o5T8slm-7}uiF zScE3yF=#e!M2qn_v>L0?X55B$<0v zz#(HY4jWhEi182{HD=+MF$eb;*WkEu9ZncaaniT}r;Nwqw6O|jj5RoG+>Ud`9XM}1 z0~d@9DEXX+8lQ{M;By%oeOl4va}An(Za|CAEok-WLYvRsX!rRiI(!~Lr%yko`8o#k1sa(1Y)C)1)F>#u-PXDTYM6*)yIi#KIz!*b1-)J9FCnn+1TZikKH~; zV~_WHQ6&u26C`<#FSK3j3nrxu5NPQziJ-8kZNHjer<;+W5cxX0&G9QU~rCw$s) z(&u`d^0^tOeQw7YpKhG>xfkbr{)O{C595N*07|}3p~m+)H2A)RM&D61`M!Z>-*?dB z`vF>gC(-8nIof@{Mu+eB==7b#G~Zt_!}q_K>FZ&k{Cy3W?dyYiz5!U^8-hi?RxI_6 zMwf3qR`{;KO5ZfB_FaWFzK3D0?@?Iin}@r63$fm}7#n=cvC+2zn|zPQX5SOB#rI@v z^{vA;-_x<(_blx2Jr_HDo3P9G672T90(*R~#$Mlc?DM?|`+aZ20pGiD(6-!bX`F@AtI=f1t)s9ZLE8>CxzCM3bLCn*D;&;unrqKO5To;?VAwgbu${boymrn%|+A;ddlv z`sHGlUjb(Ot;amSGA!`hghhUpSn9V0U4AEFh2N=I>9-53{m#T1zrSIv-vwCb*NnUT zF2{PmtFXcEI&Adoz$U+2vDxoVZ1KAXTm5>m&F?{M_j?pO{GP;4zai}MdjY%sUcnx} z*Rj`c5BB-Jhy8vZ;eg*~IOsQxLw?`lu-|`h#P1gz^;^I(KUEmz@2A6YKX07yGvTCP z5Kj4p;j~{A&iL7J)-Ms~{8Dhor4*s^D)zOF=m-sFx&KZ%rjk!1*RLZ$kd6YraRDO`Uh5+ z?!!t`A6ArBJA%k(nVoBoXrrnj)sG>%QC53$+wDYlrt#8%S`wwZpw zcGJ(;Vfr0AO;R}JZ_;A7$qRc-e%Na=W1lG$`%RHJV2Z^-lLLoL$vA9Ui6f>%aMYBA zW2PM3V_Jjbrgb=BD#c0D2Anb-i_@kmoH5nltZ6&WnRej3=?q*jHK62w9%}qALWBQh zX!LJIlm9hn_P+rw{H z{%>M||GQY^KY^wGpPwhoK`Tq;&{U62!{{fT&onPACEx?J227$Y;B&MGe2tEP z@6j1BhiL)7Vn)D!F*Cp;g7ObAV0M5H<^=>`K|lx=1z52(AR1i(@mLYC0xJX3usUEB z)&v}ewE;(AT|gf03Mj<-fMRS2D96Tt3Tz5E9-9MB#Fl`Qu{EF$+X7C<_JFgnBj8-@ z3~0ixfJ?AD;0o*sxEgx{+OaR-ChQNm4F>}5!oh$Z916G}hXWqMk$}f=G++?N0-nV^ z0Wacsz^gbBFou%>Z{t+J`#2r&G0p@`;cUQHI2Z68&IkO63jy;e1^$7WKy@VLAE-xT zpb<@h{%8&iMoVBgS_5rp3yed1U=lh4Q_&fifoXwHOhS%C$Z9k?Fz0?V)< za1#~3h5XfhvzX7fh0n2$rNxf*TeZD=>2f)4Xebeiii z&3q1Kn9s*d^Tn8DZozEx-!ad8Ef$z>#3FMimYVNCm-!!9VZIM5&3#yHegtdGPhhS2 z8LTr8<1X{dSa1F}HkjYSM)No}nLos4^QYKi{t{cwGuUSS0o%<#V~6>7>@@S`mDFX{ zVz=1~d(3{=Yc^w_ITZWNkvL$E#X++Jhs?=1Y+i{Y=0kAQoP}fN9Nc4GgX89PIAJcu zN%IDrG9Qc6<|>>q*Wj#qJI0YDH7fHE0gH0WCqd zpf#uqZ9#XVJ?Nk42zmgWLH(E(^f+b&J&l<`&tq252xbSphIv76VnNWmSQIpYr9q#d zE9eWX2>J#qgJ!Wh=qIcR`VDJ?{=~WbxOYzm6N=AamC2};1$ zASbp3rDJ>0!PpUWICchQV^>fipv~AHbOH_pZNG4Tpnv z<4DliI2zQ5V?h_UYra17tRMgj0-^n zC2B%?l@G7hcJ`8JvkHWg(Jlqvr zi1oq6*brQfjlmVz6ns232cL*7!6##Da2>Vd_N8cKZGN}kKt(WAdUq;i+h4!#PQ%)aUysOCxhR{ zso?i<{?}2SPr>!H{Vj3i%d?L;iy! zA-~{g$O4XqsO*$~hz`d?ym2DLgp(mbI296x(;-nf6Jp2NkVKpdNx}J$gK!}v6D7+L zsIja@gJmrmEk$Uu9D`=dMzmOtL#w44ZI*3lx153w%T9D!>M_l74rW-+$4tw`m}P0f zY|Gy<&vGpmSZ>53ODC3E?m(C2A6Q|z4=XKwSZ#R(Yb;M-t>qc4vkc=d%gb1A`8PIL z-oi%9I5t^6#AeH<*kbt-TP-u#X88fzEk9$2<#+6~NO6?EMT^}QFYK}SVXwuEeU?z{ zw?yKAB^C!Q4ji&1d>FC zCiFL~4gC}ALN)P}f2b$chx%eeXdpI*TCgcJ0-Hl)uq8ABTSJ}L7MhOjp$B6}=;7EI znvGqd`PdzLH1>p+U~i}k`$9Kkf9MG~5V{oyLu+v;^fVj}-HjumXX9vSBaVe$hU{l!f z*c^5uwuGIGtzmW87Ir$ehnj(uS_VSm_dI1qLh z4uhnF$LX+-aVBgEXT!e2 zxv=kWKI}(a2%ASK{14QGs~wboxE_t+Ml^-{qd7bnE#cv44Y#2!JPz&QN$3bqMQ3;h zriCAh8R17_W_T`Ug%@CU_FT;ZHO;{9OiKXFN&=r0XR)n95mEpUvI{Zwm3I7|` zhF^en;mx=!{Bo=hzX}_|ufxXh4r~g)6`RBF#Fp@Tur<6F+rl5j_V7osBm7D13?IU- z@E5Q<{1xm8e;s?n_h4W6d)OcT5e|fZhJ)eLI28UZ4u}5-N5X%>(eMQv3s)sl{^2?t z5BJ82a1&032jNtB7*2;r;Y_$4XTuY5E<6S2!wK8V%O9XtWlg$$AW$ ztsBu|Jr1qbYP4Cmq1}24I;=ap7TVJs&fz7h{&S1+%Sx$2{w`SYW*oi>#ek zYP|zp)_-7y^**e$_F=X45v;L3fwk6Wu+BP+yR0u`z4hPNV0{Z4t>f5a{Sce2pJI#k zOKi2yV4L*^Y`6Z59oFBm)5`zaD0Nx2*lqQ~9;+YrTFuyJ4aI(IBo0_(anS0(A!{-Y zTUX+U^$;AjX5pAM2lrUl;J9@iPFPEE(z*estjFTCwF+mfH8^YCj&s%>IBz`z7px5^ zMVyD4h>Oq=aTyvTTG1474VoivKug3eXpQJXTg2UHkN77#A|60zL_elQJdPO=Ph)1p z^OzMeg4q$TVP3?WSP=0p7DY^8X~ZY!iueL6BEG@Oh*_+T_z7zwe#6>`Kd~-Cvx4%E z@WlEEUu=j7#Ks5SF5-8dU@FV02$3+E#q#)XIhlp>!(P2_WEhzxP899e(k-uU_WDgES-jBnP58+7U zV>lW)h+~n@;-1JCaXj)>oQNF5$;h{HD)N1tj{F#BBByXR@++K+{0`?Mf5e5zd6c65 zKuwf7neva)qcO^erYL_jM+KuLDjcm*Hnc^>p*<=I9Z{+1jLN{Ys6#O$>PXCt%Ehdx z0?dwDk9kpLSP-=di=ryAG-?aFqE5n!s8g{rY8O^VoryJ3f5Y0S3$QM#8Fxipj`dMj zVMEk)*cjD;O;NXEbJU&K5_J!@M)hJ_)PvX_^(b~kJ&B!BL)aDd0(M8ef;~~MV{gfvC@LFlriyqQ1r9sQ=(d)Gs(1wSZ$$suapUN{8c7-Z&9u!pW#0oQev= z>8L22iL&EtR3gqrrQm$jLAVf=iIVLI)Yw*|!L}BSwjwmyjzP0+BU)_7q19H6HrqC| z+fG4;Z6`Wy^_XTm2QzHvW2Wt5%(As$w(akjXS)^)Y&T+&trJUacc9Dm53I1=hn2QI zthPOZHMS?P*7gk6*@khK?PaXD{Tmx>Z(*Zt9Gh$(Vzcd2Y_WZbt+p9#v;BbWwx6-X z_B(dkq*Th^rp0cX7xvivu-9hBK3gdE+ahtm7K?*62M*biaoDyJM{I}Ss4WY}Y&p2c zwg$&->u|zWij%etIAuE)r)^a@W2?bg+jg9@?ZA248Mt6;Kq>k>)I?u|hUm-C7~P7d z=xfj%eFIveZ$WEx7uurlMtk%>(GmRsI-~nBE&6fHh<+L~qo2pD=n>40ehu@Y-^7CG zcd;mX0!yPmL09w_SP}gVRz}Zab@WeI6a5?3M*oR*(V8^MKiU)PqkXX`2T+Q63N#XmrKIV@1pgtc*#+>X=nn6LT2W z#vFxpF?qNvrV#66im@T492;XQuqozvY>qh*TVhVe)|fhMi#Z+JW6r{km~*i+rU|=Z zF2U}YE3hZ#YV3_^$G(`Gus`ND9EiCK2V;70DCT|~j(G@2VjjcMm_Zzic^3D?yolp5 zui`|^7*58#jZ-o2<8;i&I1@94voT-cT+DYkAM+zF#LS}<`v+=b)hj9gSUnnJjcAJX zM{{g2T4KY|8f!yaY#iESlh6^Hiq6;!Op84fGh&a#%-CGaiY>tG*!7qfTZRR(o3JRh z5=&#ZpeyzytcX1oD`R(Ib?li~6Zld8 zi7m1BU~6nIw#7b(?XizyN9>c>89Rhsu`ghE>?_z4`#ScP_gqp=G(7OOgl@{iTwc&s;0#F}t2HVCI;!*Dt_3TI;NI2)UYbFnEn zAA1lk#Ac#oKLR!O)o8G{ z++}|m>+S!>2K!ssXdlNW`-j+U{}fy7Ut+6$2HWgEV7vWi?6Ciiop%2BRH@6Z#csP7 z_SpTf*KWo>dnoqXBXPhUi-UFt4%w4&*uD}+?1$i}JqyR|Ik?BZ2FLB|aKc`SllBcb zWj_|D?NvBqufbXScAT^Czy{AU4KXuqiGAo8w}zB`yJ5TPwd*Uv|@wh8-BCZW5&Dr*dvPxAUpOE4FfPOmpcMZU zYT}bi{v;&iFY@i~kif;{S`8@g4_L z{_zIPj`zX5_y8=355b~%E0)GbqboihE8S@e7~g|K@%Q6!{6jbr{}_(O58_z-v$!YzMI4WR6({1ya5DaFoQi)R zr{h1ynfNK3jsFVg;=jZB_#bg0ejcTSKTwmPK7{g5(4#TIh^7R8G$#b3B_SNG2{yDP z#GySQ2^|Tk=uF7Kw1h)3BjHHQOvuHogaXV?SdV!LWmu4~35yacu{2=|x)M&piiA_K zGGP~1C!C2j34g=dgbT1Pp&552T#oe#S7AfKb=a8DflUdwVspZs*phG$wkGsqTf&3b zp71DkBs__o2}9VG@B(%xyn;OmuVZh*9_&ka5Bn27!hwX(a4=yShZ4TU;e`L-NWw2T zny`Rl393xWKS77%3Enu7V8Y3SAe>4F!|8-5oJp|bY(gT=C8Xed!a=x@kcpDx2-G-M zqrtHjjgBHTIgUZIV1V7BA$ znCG|_3mi9Mk)sn!9e1G1@eizU+=rEpKCE^;f;EmOu-5Sm);Wf8m*Zuucl;Y09B*Nx zV;q|tA7ZoPQ*3d3iLH(qY;*j8?T(+Z!|^+II;2A>e}@*k9bVYu@WWn*8T%Ze*zbtM z0Y@wjIvhCUNXB8uN*r+*FScdWw+M=4G^HsF-wSe$lL;f$jOXC2#d z&angM9cSQzqXDJF^H7s`5gHONLt|nqni8);bK(tXNxTKEiCt(*yc_L_|3pXP1L#cb z$F#)9F(dJ5%uIY9vl2%zJMlHlOMDXx65qw5#0e}-`~+QzUtmSzH&~fCi`9ugVNK$1 zSey7K)+K5Vqx=&+u|Clk8xjMtG0}oei4oYG7=tZ|3D}zG#J0qAY)?EGI}#7a&ctl& zO3cUZ#G|n%u>^Y)UD%hn8T%7Yz=6cAIG9+ALy4#1aN=$pNjw`z6B}_X@j~2_cqxu2 zUWpTlZ8({DJx(RwjMIs?<4j^V&L-ZAbBX`L`NW5DA#nhuq^D4m^c)(JUP5EiD4LSq zKy%VNXi542tx1z;OZpt`NnfKQ>3ei0&0$*7ub7eaU(8JMIGplNGGKO+59TEWU_nv{ z7A0A+G$|TgN%2^bv;r%W(y%&d71ksjhP6pYVO>%l?n)}e`lMoPNGiw1qzY_GIv$&o zPQ;d^ld(0a4%?DW$M&SNup{YQ>`ZFHuB1z_JLwASNxB+)liIN_=_c$?x(x@C?!v*O z9vn)#ABU43!jYuMa5QNU$C94KJxMR(c+#sll_Z||olX)@>CPmHr*vnN#M8KQN#beT z1vy<>A<{J~M7m*xNH?w!&*7R@i05$4E5viSmKEYTTc1+&Z9(h~i^0E{1wkB7IXMU$- zr&rucd}f7s@^^NHc=C5nc7BC;{&!)8c>Y&%isyedvIeJk3fSlrPXU{p;wfOWQ#=K1 zaf+vatxoY2u+1r+0=7HFQ@{?Vcna9*6i)%CImJ`J8BXyOaHdl{1)Sv+PXT8;#Z$m} zPVp3Qfm1vMT;vo_0hc<*D07!nJPTame4BWsQ#=t|?G#T0*EruNrPlc|);Xtem-8#E zcYcQr&L6SSIgd@wKd{-U&Z7M|_1NkRVhwH1Fl={ZK-G)Jtu$sF%8AQ7^laMZMG~i+X8F7A4S}EJ~myS(HGl zd=zcTqRiWqMJaX2``Ib4tt(kPdEK2Xo;&VI7Ej#wCI^$VFIhY<-=7>VN;uhugYvRN z$#KMolap{nP9IH9C2mT|Ky%8WXh}H|ttq)^ODRBm%6fFjbDgqjvKc95q-UmV!mN}^ z%ud;Yc_}AhLCUFEl(Gv;Q^aw(WGiGVQ^YY=%ht%&rij|COA$xCOSWFNAw|?#8K88)VhGHgl}``?@@_P<5m|5kbb+vNRkm-oLzUVmq*XeV8%qRn*6N7N(lXP>-R z{qoiariuEhP80Q2lP2n`Hcix5 zU7Dz`0eRU$dD$U(E5q_0j>_8_lee`eP1M)8yv2z$Q8JThqGYDztxu*Sv0;Y6P0_Jp40+w`9kJfb24sGe89cHDAGRaOCWs;XJ z%A_D&lu41iwo-Xnm%MC+ylkbsY_+^>O}c1Tb@Cc^$!n;W*U%uZp;2B#le|C8^8R$l zIXmT?U2@KDIcIOWD9OHbQIh@Xq9g~>#knw;F3yFabkW-k%X3F$M`g#-MQ^i5Ugx;H z&Ix&)lkz&JT)4Mwl<+RXTloB}@_AATd9{q3kCz9@^Q)E5 zO-gvNlK#9B@)AoqzfB3BRl?^959fG3Q9i5q^*wp}O8A-9z5ZC8JKRe6q~uv3!sn&H zf{O`7x!t9te?};l_t3k;SS8F=!mUbpu@b7NSdpLqrBV)alrWc29QVaac$X4>ri8J) z?_JE_s)QFS;ay7jyb^kNx%1^J;l)aLmlA%agdX1Re3M4^en>v<(58ghN?5IgO-lHQ zuRCA1pF6Bp!X_o`Qo|x3!n)kZ%v!w+&O1Nb)yjJCIzq&`W z*6Ieb)-i2jI*#dFri+;_W4e;*YNqR$Ze+TJ>2{{OnC@XZpR;L9dRv;1rle^}t2#w> zh3XpBO{zOo�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

yJVJuyBprNe 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�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%0A@P@W)}gN=>(W@fU`Y|-5#~u17|(Qh_5n>0x~i% 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�=s5XOauI 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

Jj8>J>Yr!%zpX-^1sktt&llDKTB>2>3!IHx-*LJ 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 literal 0 HcmV?d00001 diff --git a/.task8-sweep/ref/RefClumsyDance.dll b/.task8-sweep/ref/RefClumsyDance.dll new file mode 100644 index 0000000000000000000000000000000000000000..7e191a18b59d5ae719c3ae067578e10974215d5a GIT binary patch literal 55296 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 VEwCGZJqtSY!Yls2-~W9U_+O;32^atX literal 0 HcmV?d00001 diff --git a/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj b/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj new file mode 100644 index 0000000..94e673d --- /dev/null +++ b/.task8-sweep/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0-windows + true + disable + enable + + + + + + diff --git a/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs b/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs new file mode 100644 index 0000000..65b748a --- /dev/null +++ b/.task8-sweep/tests/PathSmoothingPngVerificationHost/Program.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; + +internal static class Program +{ + private static readonly byte[] PngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + private static int Main(string[] arguments) + { + try + { + if (arguments.Length == 3 && arguments[0] == "--export-fixtures") + { + ExportFixtureReports(arguments[1], arguments[2]); + return 0; + } + if (arguments.Length == 2 && arguments[0] == "--export-end-to-end") + { + ExportEndToEndReports(arguments[1]); + return 0; + } + Require(arguments.Length == 1 && File.Exists(arguments[0]), "A fixture path is required."); + Verify(arguments[0]); + Console.WriteLine("PNG verification host completed."); + return 0; + } + catch (Exception exception) + { + Console.Error.WriteLine(exception.ToString()); + return 1; + } + } + + private static void ExportFixtureReports(string fixturePath, string outputDirectory) + { + Require(File.Exists(fixturePath), "Fixture path was not found."); + PrintReports(new PathSmoothingComparisonDemo().ExportFixtureReports(fixturePath, outputDirectory)); + } + + private static void ExportEndToEndReports(string outputDirectory) + { + PrintReports(new PathSmoothingComparisonDemo().ExportEndToEndReports(outputDirectory)); + } + + private static void PrintReports(IReadOnlyList scenarios) + { + for (int scenarioIndex = 0; scenarioIndex < scenarios.Count; scenarioIndex++) + { + PathSmoothingComparisonScenarioResult scenario = scenarios[scenarioIndex]; + if (scenario.Comparison == null) + { + Console.WriteLine(scenario.ScenarioId + " coarse=" + scenario.CoarsePathStatus + " " + scenario.Diagnostic); + continue; + } + PrintEntry(scenario.ScenarioId, scenario.Comparison.RawPathBaseline); + for (int entryIndex = 0; entryIndex < scenario.Comparison.Entries.Count; entryIndex++) + PrintEntry(scenario.ScenarioId, scenario.Comparison.Entries[entryIndex]); + Console.WriteLine(scenario.ScenarioId + " report=" + scenario.Report.Status + " " + scenario.Report.Reason); + } + } + + private static void PrintEntry(string scenarioId, PathSmoothingComparisonEntry entry) + { + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "{0} {1} status={2} length={3:F4} peakKappa={4:F4} clearance={5:F4}", + scenarioId, + entry.IsRawPathBaseline ? "RawPath" : entry.Method.ToString(), + entry.Status, + entry.Metrics.PathLengthMeters, + entry.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, + entry.Metrics.MinimumBodyClearanceMeters)); + } + + private static void Verify(string fixturePath) + { + PathSmoothingComparisonRequest request = CreateComparisonRequest(); + PathSmoothingComparisonResult comparison = new PathSmoothingComparisonService().Compare(request); + Require(!comparison.IsCancelled, "Fixture comparison was cancelled."); + Require(request.SmoothingRequest.CoarsePath.Count > 1, "Fixture coarse path is too short."); + + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + var model = new SmoothingFigureModelBuilder().Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + "straight", + "Straight"); + + VerifyObstacleRectanglesAreMerged(fixturePath, comparison, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading)); + VerifySixFigureDefinitionContract(model); + VerifyPointOnlyRendererContract(model); + + var resolver = new SmoothingFontResolver(); + Require(resolver.TryResolve("SimSun", "Times New Roman", out SmoothingFontResolution fonts, out string fontReason), + "Required report fonts are unavailable: " + fontReason); + byte[] png; + using (fonts) + { + Require(fonts.ChineseFamilyName == "SimSun", "Chinese font must resolve to exact SimSun family."); + Require(fonts.LatinFamilyName == "Times New Roman", "Latin font must resolve to exact Times New Roman family."); + Require(resolver.MeasureMixedText(fonts, "粗路径 κ(s) X (m) −π", 9f).Width > 0f, + "Mixed Chinese/Latin sample must have nonempty measured bounds."); + png = new SmoothingPngRenderer().Render(model, fonts); + } + VerifyPng(png); + + string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-png-" + Guid.NewGuid().ToString("N")); + try + { + var exporter = new SmoothingReportExporter(); + SmoothingReportExportResult report = exporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = outputDirectory, + FileStem = "straight-report", + }); + Require(report.Status == SmoothingReportExportStatus.Success, "Report export failed: " + report.Reason); + Require(report.SvgPaths.Count == 6 && report.PngPaths.Count == 6 && File.Exists(report.CsvPath), + "Successful export must publish six SVGs, six PNGs and one CSV."); + VerifyPublishedSixFigureFiles(report, outputDirectory); + Require(!ContainsTemporaryFiles(outputDirectory), "Successful export must not leave temporary files."); + + string unavailableDirectory = Path.Combine(outputDirectory, "missing-font"); + SmoothingReportExportResult unavailable = exporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = unavailableDirectory, + FileStem = "must-not-write", + ChineseFontFamilyName = "Missing report font", + LatinFontFamilyName = "Times New Roman", + }); + Require(unavailable.Status == SmoothingReportExportStatus.FontUnavailable, + "Missing exact font must report FontUnavailable."); + Require(!Directory.Exists(unavailableDirectory), "Missing-font export must remain atomic and create no output directory."); + } + finally + { + if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); + } + } + + private static PathSmoothingComparisonRequest CreateComparisonRequest() + { + PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), + ResolutionMm = 50f, + AllowExplicitEmptyMap = true, + }); + Require(mapResult.Succeeded && mapResult.Map != null, "PNG verification map must be created."); + var vehicle = new VehicleParameters + { + LengthMeters = 0.20d, + WidthMeters = 0.20d, + SafetyMarginMeters = 0d, + MaximumCurvaturePerMeter = 100d, + }; + var coarsePath = new List + { + CreatePoint(0.5d, 0.5d, 0d), + CreatePoint(1.0d, 0.5d, 0.5d), + CreatePoint(1.0d, 1.0d, 1.0d), + CreatePoint(1.5d, 1.0d, 1.5d), + }; + var segments = new List + { + new PathSegment(0, TravelDirection.Forward, 0, coarsePath.Count - 1, false, false), + }; + return new PathSmoothingComparisonRequest(new PathSmoothingRequest( + coarsePath, + segments, + mapResult.Map, + vehicle, + new PathSmoothingConfiguration())); + } + + private static void VerifySixFigureDefinitionContract(SmoothingFigureModel model) + { + SmoothingFigureSet figureSet = new SmoothingFigureSetBuilder().Build(model); + var stems = new List(); + for (int index = 0; index < figureSet.Figures.Count; index++) stems.Add(figureSet.Figures[index].FileStem); + string expected = string.Join(",", new[] + { + "01-coarse-path-overview", + "02-all-paths-comparison", + "03-cubic-bspline-overview", + "04-local-cubic-bezier-overview", + "05-piecewise-quintic-overview", + "06-curvature-comparison", + }); + Require(string.Join(",", stems) == expected, "Six-figure report stems must be stable and ordered."); + Require(!figureSet.Figures[1].ShowsMapContext, "All-path comparison must contain only trajectories, axes and legend."); + Require(figureSet.Figures[2].ShowsMapContext && figureSet.Figures[2].Series[0].Opacity < 1d, + "Individual smoother figures must retain a faded raw-path map reference."); + Require(figureSet.Figures[0].WorldScalePointsPerMeter > 0d && + Math.Abs((figureSet.Figures[0].WorldXMaxMeters - figureSet.Figures[0].WorldXMinMeters) / + (figureSet.Figures[0].WorldYMaxMeters - figureSet.Figures[0].WorldYMinMeters) - + figureSet.Figures[0].PlotWidthPoints / figureSet.Figures[0].PlotHeightPoints) < 0.000001d, + "Overhead figures must preserve equal X/Y scale."); + Require(figureSet.Figures[0].LegendYPoints - + (figureSet.Figures[0].PlotYPoints + figureSet.Figures[0].PlotHeightPoints + 31d) >= 8d, + "Legend must leave vertical clearance below the X-axis unit label."); + } + + private static void VerifyObstacleRectanglesAreMerged(string fixturePath, PathSmoothingComparisonResult comparison, Pose2D start, Pose2D goal) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); + int rectangleIndex = -1; + for (int index = 0; index < fixtures.Count; index++) + { + if (fixtures[index].Id == "rectangle-detour") { rectangleIndex = index; break; } + } + Require(rectangleIndex >= 0, "Fixture suite must include rectangle-detour for obstacle rendering verification."); + SmoothingFigureModel model = new SmoothingFigureModelBuilder().Build( + comparison, requests[rectangleIndex].SmoothingRequest.Map, start, goal, "obstacle", "obstacle"); + Require(model.Obstacles.Count > 0, "Rectangle-detour fixture must produce report obstacles."); + for (int firstIndex = 0; firstIndex < model.Obstacles.Count; firstIndex++) + { + SmoothingFigureObstacle first = model.Obstacles[firstIndex]; + for (int secondIndex = firstIndex + 1; secondIndex < model.Obstacles.Count; secondIndex++) + { + SmoothingFigureObstacle second = model.Obstacles[secondIndex]; + bool matchingColumn = Math.Abs(first.X - second.X) < 0.0000001d && Math.Abs(first.Width - second.Width) < 0.0000001d; + bool verticallyAdjacent = Math.Abs((first.Y + first.Height) - second.Y) < 0.0000001d || + Math.Abs((second.Y + second.Height) - first.Y) < 0.0000001d; + Require(!(matchingColumn && verticallyAdjacent), "Adjacent occupied rows must merge into a single obstacle rectangle."); + } + } + } + + private static void VerifyPointOnlyRendererContract(SmoothingFigureModel model) + { + SmoothingFigureDefinition figure = new SmoothingFigureSetBuilder().Build(model).Figures[1]; + string svg = new SmoothingSvgRenderer().Render(figure); + Require(svg.Contains("class=\"trajectory-point\""), "Trajectory samples must render as discrete SVG point markers."); + Require(!svg.Contains("stroke-dasharray") && !svg.Contains("-path\""), "Trajectory SVG output must not use dashed or joined path strokes."); + Require(svg.Contains("X (m)") && svg.Contains("Y (m)"), "Overhead SVG must label metre coordinate axes."); + } + + private static void VerifyPublishedSixFigureFiles(SmoothingReportExportResult report, string outputDirectory) + { + var expectedStems = new[] + { + "01-coarse-path-overview", + "02-all-paths-comparison", + "03-cubic-bspline-overview", + "04-local-cubic-bezier-overview", + "05-piecewise-quintic-overview", + "06-curvature-comparison", + }; + VerifyPublishedPaths(report.SvgPaths, expectedStems, ".svg", outputDirectory); + VerifyPublishedPaths(report.PngPaths, expectedStems, ".png", outputDirectory); + Require(!File.Exists(Path.Combine(outputDirectory, "comparison.svg")) && !File.Exists(Path.Combine(outputDirectory, "comparison.png")), + "Six-figure export must not leave legacy composite comparison images."); + } + + private static void VerifyPublishedPaths(IReadOnlyList paths, string[] expectedStems, string extension, string outputDirectory) + { + var actual = new List(); + for (int index = 0; index < paths.Count; index++) actual.Add(paths[index]); + Require(actual.Count == expectedStems.Length, "Six-figure export must publish six " + extension + " files."); + for (int index = 0; index < expectedStems.Length; index++) + { + string expected = Path.Combine(outputDirectory, expectedStems[index] + extension); + Require(actual[index] == expected && File.Exists(actual[index]), "Published " + extension + " path must match the stable figure stem."); + } + } + + private static CoarsePathPoint CreatePoint(double x, double y, double arcLength) + { + return new CoarsePathPoint( + x, y, 0d, 0d, arcLength, TravelDirection.Forward, 0d, 1d, false, CoarsePathPointSource.Start); + } + + private static bool ContainsTemporaryFiles(string directory) + { + foreach (string ignored in Directory.EnumerateFiles(directory, "*.tmp")) return true; + return false; + } + + private static void VerifyPng(byte[] png) + { + Require(png != null && png.Length > PngSignature.Length, "PNG output is empty."); + byte[] data = png ?? throw new InvalidOperationException("PNG output is empty."); + for (int index = 0; index < PngSignature.Length; index++) + Require(data[index] == PngSignature[index], "PNG signature is invalid."); + + bool sawHeader = false; + bool sawPhysicalResolution = false; + int offset = PngSignature.Length; + while (offset < data.Length) + { + Require(offset + 12 <= data.Length, "PNG chunk header is truncated."); + int length = checked((int)ReadUInt32BigEndian(data, offset)); + int dataStart = offset + 8; + int crcStart = checked(dataStart + length); + Require(crcStart + 4 <= data.Length, "PNG chunk data is truncated."); + uint expectedCrc = ReadUInt32BigEndian(data, crcStart); + uint actualCrc = ComputeCrc32(data, offset + 4, length + 4); + Require(expectedCrc == actualCrc, "PNG chunk CRC is invalid."); + + string type = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4); + if (type == "IHDR") + { + Require(length == 13, "IHDR length must be 13."); + Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.WidthPixels, "PNG width must be 4296."); + Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.HeightPixels, "PNG height must be 3120."); + sawHeader = true; + } + else if (type == "pHYs") + { + Require(length == 9, "pHYs length must be 9."); + Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.PixelsPerMeter, + "PNG horizontal density must be 23622 pixels/meter."); + Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.PixelsPerMeter, + "PNG vertical density must be 23622 pixels/meter."); + Require(data[dataStart + 8] == 1, "PNG pHYs unit must be meter."); + sawPhysicalResolution = true; + } + offset = crcStart + 4; + } + Require(sawHeader && sawPhysicalResolution, "PNG must contain IHDR and pHYs chunks."); + } + + private static uint ReadUInt32BigEndian(byte[] data, int offset) + { + return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | + ((uint)data[offset + 2] << 8) | data[offset + 3]; + } + + private static uint ComputeCrc32(byte[] data, int offset, int length) + { + uint crc = 0xffffffffu; + for (int index = 0; index < length; index++) + { + crc ^= data[offset + index]; + for (int bit = 0; bit < 8; bit++) + crc = (crc & 1u) == 0u ? crc >> 1 : (crc >> 1) ^ 0xedb88320u; + } + return crc ^ 0xffffffffu; + } + + private static void Require(bool condition, string message) + { + if (!condition) throw new InvalidOperationException(message); + } +} diff --git a/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 b/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 new file mode 100644 index 0000000..9f8223a --- /dev/null +++ b/.task8-sweep/tests/generate_path_smoothing_fixtures.ps1 @@ -0,0 +1,15 @@ +param( + [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), + [string]$OutputPath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), + [switch]$Overwrite +) + +$ErrorActionPreference = 'Stop' +$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +$type = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.SmoothingFixtureGenerator', $true) +$method = $type.GetMethod('Generate', [Type[]]@([string], [bool])) +if ($null -eq $method) { throw 'SmoothingFixtureGenerator.Generate(string, bool) is missing.' } +$method.Invoke($null, @([IO.Path]::GetFullPath($OutputPath), [bool]$Overwrite)) +Write-Output "Path smoothing fixtures generated: $OutputPath" diff --git a/.task8-sweep/tests/run_path_smoothing_comparison.ps1 b/.task8-sweep/tests/run_path_smoothing_comparison.ps1 new file mode 100644 index 0000000..2308ccd --- /dev/null +++ b/.task8-sweep/tests/run_path_smoothing_comparison.ps1 @@ -0,0 +1,32 @@ +param( + [switch]$FixtureOnly, + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports') +) + +$ErrorActionPreference = 'Stop' + +$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path +$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) +$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar +if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Report output must stay below $allowedRoot" +} + +$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' +$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path + +& dotnet build $projectPath --no-restore +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +& dotnet run --project $hostProject --no-restore -- --export-fixtures $resolvedFixturePath $resolvedOutputDirectory +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +if (-not $FixtureOnly) { + & dotnet run --project $hostProject --no-restore -- --export-end-to-end $resolvedOutputDirectory + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +Write-Output "Path smoothing reports written below $resolvedOutputDirectory" diff --git a/.task8-sweep/tests/verify_coarse_path_collision.ps1 b/.task8-sweep/tests/verify_coarse_path_collision.ps1 new file mode 100644 index 0000000..b7cd515 --- /dev/null +++ b/.task8-sweep/tests/verify_coarse_path_collision.ps1 @@ -0,0 +1,130 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function New-TestMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $rectangleType = $assembly.GetType($mapping + 'AxisAlignedRectangleObstacle', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) + $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1500, [single]1550, [single]1200, [single]1800)) + $obstacles = [Array]::CreateInstance($obstacleType, 1) + $obstacles.SetValue($obstacle, 0) + $source = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue($source, 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Test map must be created.' + Assert-True $result.Map.PlanningReady 'Test map must be ready.' + return $result.Map +} + +function New-DiagonalCellMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) + $obstacle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) + $obstacles = [Array]::CreateInstance($obstacleType, 1) + $obstacles.SetValue($obstacle, 0) + $source = [Activator]::CreateInstance($manualType, @('diagonal-cell', [long]1, $true, $obstacles)) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue($source, 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Diagonal cell map must be created.' + Assert-True $result.Map.PlanningReady 'Diagonal cell map must be ready.' + Assert-True $result.Map.IsOccupied(30, 30) 'Diagonal cell obstacle must occupy its 50mm cell.' + return $result.Map +} + +$map = New-TestMap +$vehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$vehicle.LengthMeters = 0.20 +$vehicle.WidthMeters = 0.20 +$vehicle.SafetyMarginMeters = 0.0 +$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker + +$diagonalCellMap = New-DiagonalCellMap +$diagonalCellVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$diagonalCellVehicle.LengthMeters = 0.10 +$diagonalCellVehicle.WidthMeters = 0.10 +$diagonalCellVehicle.SafetyMarginMeters = 0.0 +$diagonalCellPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.299, 1.299, 0.0) +$diagonalCellClearance = 0.0 +$diagonalCellSafe = $checker.IsPoseCollisionFree($diagonalCellPose, $diagonalCellMap, $diagonalCellVehicle, 0.20, [ref]$diagonalCellClearance) +Assert-False $diagonalCellSafe 'Expanded diagonal footprint must not bypass an occupied 50mm cell.' + +$edgePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.35, 1.50, 0.0) +$edgeClearance = 0.0 +$edgeSafe = $checker.IsPoseCollisionFree($edgePose, $map, $vehicle, 0.0, [ref]$edgeClearance) +Assert-False $edgeSafe 'Touching an occupied cell must be a collision.' + +$farPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.50, 0.50, 0.0) +$farClearance = 0.0 +$farSafe = $checker.IsPoseCollisionFree($farPose, $map, $vehicle, 0.0, [ref]$farClearance) +Assert-True $farSafe 'Strict distance field clearance must allow the distant pose.' +Assert-True ($farClearance -gt 0.0) 'Distant pose must report positive body clearance.' + +$from = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.20, 1.50, 0.0) +$to = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.80, 1.50, 0.0) +$sweepClearance = 0.0 +$sweepSafe = $checker.IsSweptMotionCollisionFree($from, $to, $map, $vehicle, 0.50, [ref]$sweepClearance) +Assert-False $sweepSafe 'A swept vehicle must not pass through an occupied cell.' + +$gridCenterPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.475, 1.475, 0.0) +$gridCenterClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($gridCenterPose, $map, $vehicle, 0.0, [ref]$gridCenterClearance) 'A pose at an occupied grid center must collide.' + +$subGridPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.481, 1.463, 0.37) +$subGridClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($subGridPose, $map, $vehicle, 0.0, [ref]$subGridClearance) 'An arbitrary sub-grid heading must collide with the thin obstacle.' + +$diagonalPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.32, 1.50, ([Math]::PI / 4.0)) +$diagonalClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($diagonalPose, $map, $vehicle, 0.0, [ref]$diagonalClearance) 'A 45 degree footprint overlap must collide.' + +$outsidePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.05, 0.05, 0.0) +$outsideClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($outsidePose, $map, $vehicle, 0.0, [ref]$outsideClearance) 'Any footprint corner outside the map must collide.' + +$curveVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$curveVehicle.MaximumCurvaturePerMeter = 0.80 +$curveVehicle.MinimumTurningRadiusMeters = 2.0 +$maximumCurvature = 0.0 +$hasMaximumCurvature = [MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.VehicleKinematics]::TryGetMaximumCurvaturePerMeter($curveVehicle, [ref]$maximumCurvature) +Assert-True $hasMaximumCurvature 'Vehicle curvature constraints must be accepted.' +Assert-Near 0.50 $maximumCurvature 'Both curvature constraints must use the conservative minimum.' + +Write-Output 'Coarse path collision checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_integration.ps1 b/.task8-sweep/tests/verify_coarse_path_integration.ps1 new file mode 100644 index 0000000..6051426 --- /dev/null +++ b/.task8-sweep/tests/verify_coarse_path_integration.ps1 @@ -0,0 +1,460 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' + +$plannerRoot = Join-Path $PSScriptRoot '..\ParkrobTrajplanner' +$coarsePathReadme = Join-Path $plannerRoot 'CoarsePath\README.md' +$legacyRootReadme = Join-Path $plannerRoot 'README.md' +if (-not (Test-Path -LiteralPath $coarsePathReadme -PathType Leaf)) { + throw 'CoarsePath module README must be located inside the CoarsePath directory.' +} +if (Test-Path -LiteralPath $legacyRootReadme -PathType Leaf) { + throw 'The coarse-path-only README must not remain at the ParkrobTrajplanner root.' +} +$coarsePathReadmeContent = Get-Content -LiteralPath $coarsePathReadme -Raw +if (-not $coarsePathReadmeContent.Contains('## 总预算与取消')) { + throw 'CoarsePath README must document total timeout and cancellation semantics.' +} +if (-not $coarsePathReadmeContent.Contains('PlanningMapBuildStatus')) { + throw 'CoarsePath README must document map-stage termination status handling.' +} + +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods()) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { + $matches = $false + break + } + } + if ($matches) { return $candidate } + } + return $null +} + +function New-EmptyPlanningMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $request.ResolutionMm = [single]50 + $request.AllowExplicitEmptyMap = $true + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Empty integration map must be created.' + Assert-True $result.Map.PlanningReady 'Explicit empty integration map must be ready.' + return $result.Map +} + +$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$plannerType = $assembly.GetType($coarsePath + 'HybridAStarPlanner', $true) +$planner = [Activator]::CreateInstance($plannerType) +$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) +$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) +$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) +$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) +$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) +$plan = Find-Method $plannerType 'Plan' @($requestType, [Threading.CancellationToken]) +Assert-True ($plan -ne $null) 'HybridAStarPlanner must expose Plan(request, cancellationToken).' + +$defaultConfiguration = [Activator]::CreateInstance($configurationType) +Assert-Near 0.15 $defaultConfiguration.GoalPositionToleranceMeters 'Default goal-position tolerance must retain the documented 0.15 m terminal acceptance radius.' + +$request = [Activator]::CreateInstance($requestType) +$request.Map = New-EmptyPlanningMap +$request.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) +$request.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) +$request.Vehicle = [Activator]::CreateInstance($vehicleType) +$request.Vehicle.LengthMeters = 0.20 +$request.Vehicle.WidthMeters = 0.20 +$request.Vehicle.SafetyMarginMeters = 0.0 +$request.Vehicle.MaximumCurvaturePerMeter = 1.0 +$request.Configuration = [Activator]::CreateInstance($configurationType) +$request.Configuration.MaximumExpandedNodes = 10000 +$request.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) +$request.Configuration.GoalPositionToleranceMeters = 0.001 +$request.Configuration.GoalHeadingToleranceRadians = 0.001 +$request.Configuration.AllowReverse = $false +$request.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') +$request.StartVehicleCurvature = [double]0.20 + +$result = $plan.Invoke($planner, @($request, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $result.Status.ToString() 'Planner must publish a validated direct path on an empty map.' +Assert-True ($result.Path.Count -ge 2) 'Validated path must retain its start and goal points.' +Assert-Near 0.0 $result.Path[0].ArcLength 'The first path point must have zero arc length.' +Assert-Near 0.20 $result.Path[0].VehicleCurvature ` + 'The assembled start point must retain the requested physical vehicle curvature.' +Assert-Equal 'Start' $result.Path[0].Source.ToString() 'The first path point must retain the start source.' +Assert-Equal 'GoalTruncation' $result.Path[-1].Source.ToString() 'The terminal truncated point must retain its source.' +Assert-True ($result.Diagnostics.PeakOpenListCount -ge 1) 'Planner diagnostics must retain the actual Open List peak count.' +Assert-True ($result.Diagnostics.StaleOpenListEntryCount -ge 0) 'Planner diagnostics must retain a non-negative stale Open List count.' +Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) 'Planner diagnostics must expose path-search elapsed time.' +Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Successful planning must retain non-negative path-search time.' +Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) 'Path-search time must not exceed total elapsed time.' + +$nodeLimitedRequest = [Activator]::CreateInstance($requestType) +$nodeLimitedRequest.Map = $request.Map +$nodeLimitedRequest.Start = $request.Start +$nodeLimitedRequest.Goal = $request.Goal +$nodeLimitedRequest.Vehicle = $request.Vehicle +$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType) +$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0 +$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2) +$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001 +$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001 +$nodeLimitedRequest.Configuration.AllowReverse = $false +$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') +$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() 'A zero node limit must fail after planner preflight.' +Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) 'Search-stage node-limit failure must retain path-search time.' +Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) 'Failed path-search time must not exceed total elapsed time.' +Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) 'Planner diagnostics must retain a node-limit reason.' + +for ($index = 1; $index -lt $result.Path.Count; $index++) { + $previous = $result.Path[$index - 1] + $current = $result.Path[$index] + Assert-True ($current.ArcLength -ge $previous.ArcLength) 'Path arc length must be non-decreasing.' + Assert-True ([Math]::Abs($current.UnwrappedHeading - $previous.UnwrappedHeading) -le ([Math]::PI + 0.000001)) 'Unwrapped heading must remain continuous between adjacent points.' + $samePose = ([Math]::Abs($current.X - $previous.X) -le 0.000001) -and + ([Math]::Abs($current.Y - $previous.Y) -le 0.000001) -and + ([Math]::Abs($current.Heading - $previous.Heading) -le 0.000001) -and + ([Math]::Abs($current.ArcLength - $previous.ArcLength) -le 0.000001) + if ($samePose) { + Assert-True (($current.Direction -ne $previous.Direction) -and $current.IsGearSwitchPoint) 'Adjacent duplicate points are permitted only as a marked gear-switch pair.' + } +} + +Assert-True ($result.Segments.Count -ge 1) 'Validated path must include inclusive direction segments.' +Assert-Equal 0 $result.Segments[0].StartIndex 'The first segment must include the first path point.' +Assert-Equal ($result.Path.Count - 1) $result.Segments[-1].EndIndex 'The final segment must include the final path point.' +for ($index = 0; $index -lt $result.Segments.Count; $index++) { + $segment = $result.Segments[$index] + Assert-Equal $index $segment.SegmentIndex 'Segment indexes must be contiguous.' + Assert-True ($segment.StartIndex -le $segment.EndIndex) 'Each segment must use inclusive ordered indexes.' + if ($index -gt 0) { + $previousSegment = $result.Segments[$index - 1] + Assert-Equal ($previousSegment.EndIndex + 1) $segment.StartIndex 'Segments must cover each path point exactly once.' + } +} + +Write-Output 'Coarse path integration checks passed.' + +# Facade checks intentionally use only ASCII text so the script can also run in legacy PowerShell hosts. +$facade = $coarsePath + 'Facade.' +$serviceType = $assembly.GetType($facade + 'CoarsePathPlanningService', $true) +$jobType = $assembly.GetType($facade + 'CoarsePathPlanningJob', $true) +$jobResultType = $assembly.GetType($facade + 'CoarsePathPlanningJobResult', $true) +$debugOptionsType = $assembly.GetType($facade + 'PlanningDebugOptions', $true) +$debugSinkType = $assembly.GetType($facade + 'IPlanningDebugSink', $true) +$mapRequestType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true) +$boundsType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true) +$servicePlan = Find-Method $serviceType 'Plan' @($jobType, [Threading.CancellationToken]) +Assert-True ($servicePlan -ne $null) 'CoarsePathPlanningService must expose Plan(job, cancellationToken).' +Assert-True ($jobResultType.GetProperty('MapResult') -ne $null) 'Facade result must retain the map result.' +Assert-True ($jobResultType.GetProperty('PlanningResult') -ne $null) 'Facade result must retain the planning result.' +Assert-True ($jobResultType.GetProperty('DebugDiagnostics') -ne $null) 'Facade result must retain debug diagnostics.' + +function New-FacadeJob { + $job = [Activator]::CreateInstance($jobType) + $mapRequest = [Activator]::CreateInstance($mapRequestType) + $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $mapRequest.ResolutionMm = [single]50 + $mapRequest.AllowExplicitEmptyMap = $true + $job.MapRequest = $mapRequest + $job.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) + $job.Goal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) + $job.Vehicle = [Activator]::CreateInstance($vehicleType) + $job.Vehicle.LengthMeters = 0.20 + $job.Vehicle.WidthMeters = 0.20 + $job.Vehicle.SafetyMarginMeters = 0.0 + $job.Vehicle.MaximumCurvaturePerMeter = 1.0 + $job.Configuration = [Activator]::CreateInstance($configurationType) + $job.Configuration.MaximumExpandedNodes = 10000 + $job.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) + $job.Configuration.GoalPositionToleranceMeters = 0.001 + $job.Configuration.GoalHeadingToleranceRadians = 0.001 + $job.Configuration.AllowReverse = $false + $job.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') + return $job +} + +$service = [Activator]::CreateInstance($serviceType) +$firstFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) +Assert-True $firstFacadeResult.MapResult.Succeeded 'Facade must build the requested map first.' +Assert-Equal 'Success' $firstFacadeResult.PlanningResult.Status.ToString() 'Facade must pass a successful map to the planner.' +Assert-Equal 0 $firstFacadeResult.DebugDiagnostics.Count 'Default debug sink must not add diagnostics.' + +$secondFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), [Threading.CancellationToken]::None)) +Assert-True $secondFacadeResult.MapResult.Succeeded 'Repeated facade request must retain a map result.' +Assert-Equal 'Input' $secondFacadeResult.MapResult.CacheHit.ToString() 'One facade service must retain its map factory cache.' +Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $secondFacadeResult.MapResult.Map.InputFingerprint 'Map fingerprint must remain stable for equal input.' +Assert-Equal $firstFacadeResult.PlanningResult.Status $secondFacadeResult.PlanningResult.Status 'Map cache reuse must not change planning status.' +Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $secondFacadeResult.PlanningResult.Path.Count 'Map cache reuse must not change path point count.' + +$invalidJob = [Activator]::CreateInstance($jobType) +$invalidJob.MapRequest = [Activator]::CreateInstance($mapRequestType) +$mapFailureResult = $servicePlan.Invoke($service, @($invalidJob, [Threading.CancellationToken]::None)) +Assert-False $mapFailureResult.MapResult.Succeeded 'Invalid map input must be returned as a map failure.' +Assert-Equal 'InvalidMap' $mapFailureResult.PlanningResult.Status.ToString() 'Map failure must return an empty non-search planning result.' +Assert-Equal 0 $mapFailureResult.PlanningResult.Diagnostics.ExpandedNodeCount 'Map failure must not start the search.' +Assert-Equal 0 $mapFailureResult.PlanningResult.Path.Count 'Map failure must not publish a path.' +Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed 'Map failure must report zero path-search time.' + +$cancelledFacadeSource = New-Object Threading.CancellationTokenSource +$cancelledFacadeSource.Cancel() +$cancelledFacadeResult = $servicePlan.Invoke($service, @((New-FacadeJob), $cancelledFacadeSource.Token)) +Assert-Equal 'Cancelled' $cancelledFacadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' +Assert-Equal 'Cancelled' $cancelledFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' +Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' +Assert-Equal 0 $cancelledFacadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' + +$timedOutFacadeJob = New-FacadeJob +$timedOutFacadeJob.Configuration.SearchTimeout = [TimeSpan]::Zero +$timedOutFacadeResult = $servicePlan.Invoke($service, @($timedOutFacadeJob, [Threading.CancellationToken]::None)) +Assert-Equal 'TimedOut' $timedOutFacadeResult.MapResult.Status.ToString() 'Facade total timeout must stop before publishing a map.' +Assert-Equal 'SearchTimeout' $timedOutFacadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage timeout to planning timeout.' +Assert-Equal 0 $timedOutFacadeResult.PlanningResult.Path.Count 'Timed out facade planning must publish no path.' + +$defaultDebugOptions = [Activator]::CreateInstance($debugOptionsType) +Assert-False $defaultDebugOptions.Enabled 'Debug output must be opt-in.' +Assert-True ($defaultDebugOptions.Sink -ne $null) 'Debug options must default to a no-op sink.' + +$debugSinkSource = @' +using System; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +public sealed class FacadeRecordingDebugSink : IPlanningDebugSink +{ + public int PublishCount { get; private set; } + public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) + { + PublishCount++; + } +} + +public sealed class FacadeThrowingDebugSink : IPlanningDebugSink +{ + public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) + { + throw new InvalidOperationException("debug sink failure"); + } +} +'@ +$runtimeDirectory = [Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() +$debugSinkReferences = @( + (Join-Path $runtimeDirectory 'mscorlib.dll'), + (Join-Path $runtimeDirectory 'System.dll'), + (Join-Path $runtimeDirectory 'System.Core.dll'), + $AssemblyPath, + 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Facades\netstandard.dll' +) +Add-Type -TypeDefinition $debugSinkSource -ReferencedAssemblies $debugSinkReferences +$recordingSinkType = 'FacadeRecordingDebugSink' -as [type] +$throwingSinkType = 'FacadeThrowingDebugSink' -as [type] + +$disabledDebugOptions = [Activator]::CreateInstance($debugOptionsType) +$recordingSink = [Activator]::CreateInstance($recordingSinkType) +$disabledDebugOptions.Sink = $recordingSink +$disabledJob = New-FacadeJob +$disabledJob.DebugOptions = $disabledDebugOptions +$disabledResult = $servicePlan.Invoke($service, @($disabledJob, [Threading.CancellationToken]::None)) +Assert-Equal 0 $recordingSink.PublishCount 'Disabled debug output must not publish to the sink.' +Assert-Equal $firstFacadeResult.PlanningResult.Status $disabledResult.PlanningResult.Status 'Disabled debug output must not change planning status.' + +$throwingDebugOptions = [Activator]::CreateInstance($debugOptionsType) +$throwingDebugOptions.Enabled = $true +$throwingDebugOptions.Sink = [Activator]::CreateInstance($throwingSinkType) +$throwingJob = New-FacadeJob +$throwingJob.DebugOptions = $throwingDebugOptions +$throwingResult = $servicePlan.Invoke($service, @($throwingJob, [Threading.CancellationToken]::None)) +Assert-Equal $firstFacadeResult.MapResult.Map.InputFingerprint $throwingResult.MapResult.Map.InputFingerprint 'Debug sink errors must not change the map fingerprint.' +Assert-Equal $firstFacadeResult.PlanningResult.Status $throwingResult.PlanningResult.Status 'Debug sink errors must not change planning status.' +Assert-Equal $firstFacadeResult.PlanningResult.Path.Count $throwingResult.PlanningResult.Path.Count 'Debug sink errors must not change the path.' +Assert-Equal 1 $throwingResult.DebugDiagnostics.Count 'Debug sink errors must be retained as diagnostics.' + +Write-Output 'Coarse path facade checks passed.' + +# P1 UI scenario factory contract checks. These run against the same built assembly +# and intentionally fail until the factory is introduced. +$testNamespace = $coarsePath + 'Test.' +$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) +$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) +Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' +Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' + +$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) +$factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @( + $scenarioEnumType, [double], [double], [double]) +$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( + [double], [double], [double], [double], [double], [double]) +Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' +Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).' +Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' + +$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') +foreach ($scenarioName in $scenarioNames) { + $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) + $jobA = $factoryCreate.Invoke($null, @($scenario)) + $jobB = $factoryCreate.Invoke($null, @($scenario)) + Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." + Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." +} + +foreach ($scenarioName in $scenarioNames) { + $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) + $liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0)) + Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X." + Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y." + Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading." +} + +$rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour') +$liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0)) +Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.' +Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.' +Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.' +Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.' +Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.' +Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.' +Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.' +Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.' +$rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld() +$rectangleObstacle = $rectangleProjection.Obstacles[0] +Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.' +Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.' + +$multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg') +$baselineMulti = $factoryCreate.Invoke($null, @($multiScenario)) +$liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0)) +Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.' +Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.' +Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.' +$baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles +$liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles +Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.' +Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.' + +$noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath') +$liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0)) +Assert-Near 9.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.' +Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.' +Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.' + +$invalidLivePoseRejected = $false +try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) } +catch [ArgumentOutOfRangeException] { + $invalidLivePoseRejected = $true +} +catch [Reflection.TargetInvocationException] { + $invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] +} +Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.' + +$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) +Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' +Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' +Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' +Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' +Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' +Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' + +$slowFeasibleJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0)) +$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30) +$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000 +$slowFeasibleService = [Activator]::CreateInstance($serviceType) +$slowFeasibleResult = $servicePlan.Invoke($slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() 'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.' +Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le $slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) 'Slow feasible path-search time must remain within total elapsed time.' + +$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false) +$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false) +Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.' +Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.' + +$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double]) +$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double]) +$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object { + $_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8 +} | Select-Object -First 1 +Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.' +Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.' +Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.' + +$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2) +$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0) +$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1) +$manualObstacleJob = $manualObstacleFactory.Invoke($null, @( + 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77)) +Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.' +Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.' +Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.' +Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.' +Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.' +Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.' + +$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0) +$emptyManualJob = $manualObstacleFactory.Invoke($null, @( + 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0)) +Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.' +Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.' + +$invalidGeometryRejected = $false +try { + $null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0)) +} +catch [ArgumentOutOfRangeException] { $invalidGeometryRejected = $true } +catch [Reflection.TargetInvocationException] { + $invalidGeometryRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] +} +Assert-True $invalidGeometryRejected 'Invalid manual geometry must report argument range.' + +$scenarioService = [Activator]::CreateInstance($serviceType) +foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { + $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) + $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) + Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." +} + +$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) +$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) +Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' + +$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) +$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) +Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' +Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' +Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) 'No-path diagnostics must retain the exact search exhaustion reason.' +Assert-True (-not [string]::IsNullOrWhiteSpace($noPathResult.PlanningResult.Diagnostics.TerminationReason)) 'No-path diagnostics must retain a reason.' + +$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) +$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) +$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) +$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) +Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' +Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' + +Write-Output 'Coarse path P1 scenario checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_search.ps1 b/.task8-sweep/tests/verify_coarse_path_search.ps1 new file mode 100644 index 0000000..3566c91 --- /dev/null +++ b/.task8-sweep/tests/verify_coarse_path_search.ps1 @@ -0,0 +1,411 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Null($Actual, [string]$Message) { + if ($null -ne $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Throws([scriptblock]$Action, [string]$Message) { + try { + & $Action + } + catch { + return + } + + throw $Message +} + +function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods()) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { + $matches = $false + break + } + } + if ($matches) { return $candidate } + } + return $null +} + +function Find-NonPublicStaticMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Static,NonPublic')) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } + } + if ($matches) { return $candidate } + } + return $null +} + +function New-TestMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.AllowExplicitEmptyMap = $true + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Test map must be created.' + Assert-True $result.Map.PlanningReady 'Test map must be ready.' + return $result.Map +} + +function New-CornerBlockedMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]150, [single]0, [single]150)) + $obstacles = [Array]::CreateInstance($obstacleType, 2) + $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]75, [single]25, [single]0)), 0) + $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]25, [single]75, [single]0)), 1) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue([Activator]::CreateInstance($manualType, @('corner-blocks', [long]1, $true, $obstacles)), 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Corner-blocked map must be created.' + Assert-True $result.Map.PlanningReady 'Corner-blocked map must be ready.' + return $result.Map +} + +$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$search = $coarsePath + 'Search.' +$primitiveType = $assembly.GetType($search + 'MotionPrimitive', $true) +$generatorType = $assembly.GetType($search + 'MotionPrimitiveGenerator', $true) +$goalCheckerType = $assembly.GetType($search + 'GoalToleranceChecker', $true) + +Assert-True ($primitiveType.GetProperty('ActualLengthMeters') -ne $null) 'MotionPrimitive must expose actual length.' +Assert-True ($primitiveType.GetProperty('Points') -ne $null) 'MotionPrimitive must expose integration points.' +Assert-True ($primitiveType.GetProperty('IsGoalTruncation') -ne $null) 'MotionPrimitive must expose goal truncation state.' + +$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) +$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) +$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) +$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) +$directionType = $assembly.GetType($coarsePath + 'TravelDirection', $true) +$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) +$generate = Find-Method $generatorType 'Generate' @($poseType, [double], $directionType, $requestType) +Assert-True ($generate -ne $null) 'MotionPrimitiveGenerator must expose Generate(start, curvature, direction, request).' +$isSatisfied = Find-Method $goalCheckerType 'IsSatisfied' @($poseType, $poseType, $configurationType, $directionType, $goalDirectionType) +Assert-True ($isSatisfied -ne $null) 'GoalToleranceChecker must expose the planned goal check.' + +$map = New-TestMap +$vehicle = [Activator]::CreateInstance($vehicleType) +$vehicle.LengthMeters = 0.20 +$vehicle.WidthMeters = 0.20 +$vehicle.SafetyMarginMeters = 0.0 +$vehicle.MaximumCurvaturePerMeter = 1.0 +$config = [Activator]::CreateInstance($configurationType) +$config.PrimitiveLengthMeters = 0.50 +$config.IntegrationStepMeters = 0.05 +$config.MaximumCollisionCheckStepMeters = 0.025 +$config.GoalPositionToleranceMeters = 0.001 +$config.GoalHeadingToleranceRadians = 0.001 +$config.CurvatureLevelCount = 5 +$forward = [Enum]::Parse($directionType, 'Forward') +$reverse = [Enum]::Parse($directionType, 'Reverse') +$anyDirection = [Enum]::Parse($goalDirectionType, 'Any') +$forwardOnly = [Enum]::Parse($goalDirectionType, 'Forward') +$start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) +$generator = [Activator]::CreateInstance($generatorType) + +$request = [Activator]::CreateInstance($requestType) +$request.Map = $map +$request.Vehicle = $vehicle +$request.Configuration = $config +$request.Goal = [Activator]::CreateInstance($poseType, @(3.0, 1.0, 0.0)) +$request.GoalDirection = $anyDirection +$straight = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($straight -ne $null) 'Straight primitive must be generated.' +Assert-Near 0.50 $straight.ActualLengthMeters 'Straight primitive must use the configured cap.' +Assert-Near 1.50 $straight.End.X 'Straight primitive must use analytic integration.' +Assert-Near 1.00 $straight.End.Y 'Straight primitive must not drift laterally.' +Assert-Near 0.00 $straight.End.Heading 'Straight primitive must keep heading.' +Assert-True ($straight.Points.Count -ge 20) 'Point spacing must honor the collision check step.' +$previous = $start +foreach ($point in $straight.Points) { + $distance = [Math]::Sqrt(($point.X - $previous.X) * ($point.X - $previous.X) + ($point.Y - $previous.Y) * ($point.Y - $previous.Y)) + Assert-True ($distance -le 0.025001) 'Point spacing must not exceed the map-safe collision step.' + $previous = $point +} + +$curve = $generate.Invoke($generator, @($start, 1.0, $forward, $request)) +Assert-True ($curve -ne $null) 'Arc primitive must be generated.' +Assert-Near ([Math]::Sin(0.50) + 1.0) $curve.End.X 'Arc X must use analytic integration.' +Assert-Near (1.0 - [Math]::Cos(0.50) + 1.0) $curve.End.Y 'Arc Y must use analytic integration.' +Assert-Near 0.50 $curve.End.Heading 'Arc heading must use signed distance.' + +$reversePrimitive = $generate.Invoke($generator, @($start, 0.0, $reverse, $request)) +Assert-True ($reversePrimitive -ne $null) 'Reverse primitive must be generated.' +Assert-Near 0.50 $reversePrimitive.End.X 'Reverse straight primitive must move behind the vehicle.' + +$request.Goal = [Activator]::CreateInstance($poseType, @(1.30, 1.0, 0.0)) +$truncated = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($truncated -ne $null) 'Goal-truncated primitive must be generated.' +Assert-Near 0.30 $truncated.ActualLengthMeters 'A 0.30m goal must truncate at its first internal point.' +Assert-True $truncated.IsGoalTruncation 'Truncated primitive must record its goal source.' +Assert-Near 1.30 $truncated.End.X 'Truncated primitive must end at the goal point.' + +$request.Goal = $start +$zeroLength = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($zeroLength -ne $null) 'Start-at-goal must produce a zero-length candidate.' +Assert-Near 0.0 $zeroLength.ActualLengthMeters 'Start-at-goal candidate must have zero length.' +Assert-True $zeroLength.IsGoalTruncation 'Start-at-goal candidate must have goal truncation source.' + +$wrapPose = [Activator]::CreateInstance($poseType, @(1.0, 1.0, (-[Math]::PI + 0.0005))) +$wrapGoal = [Activator]::CreateInstance($poseType, @(1.0, 1.0, ([Math]::PI - 0.0005))) +$config.GoalHeadingToleranceRadians = 0.002 +Assert-True $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $forward, $forwardOnly)) 'Goal heading tolerance must wrap across pi.' +Assert-False $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $reverse, $forwardOnly)) 'Goal direction constraint must reject reverse entry.' + +$levelsMethod = Find-Method $generatorType 'GetCurvatureLevels' @($vehicleType, $configurationType) +Assert-True ($levelsMethod -ne $null) 'MotionPrimitiveGenerator must expose curvature levels.' +$levels = $levelsMethod.Invoke($generator, @($vehicle, $config)) +Assert-True ($levels.Count -eq 5) 'Five configured curvature levels must be generated.' +Assert-Near -1.0 $levels[0] 'First curvature level must be negative maximum.' +Assert-Near 0.0 $levels[2] 'Middle curvature level must be straight.' +Assert-Near 1.0 $levels[4] 'Last curvature level must be positive maximum.' +$adjacent = Find-Method $generatorType 'AreCurvatureLevelsAdjacent' @([int], [int]) +Assert-True ($adjacent -ne $null) 'MotionPrimitiveGenerator must expose curvature adjacency.' +Assert-True $adjacent.Invoke($null, @(2, 3)) 'Neighboring curvature levels must be allowed.' +Assert-False $adjacent.Invoke($null, @(2, 4)) 'Non-neighboring curvature levels must be rejected.' + +$heapGenericType = $assembly.GetType($search + 'BinaryMinHeap`1', $true) +$heapType = $heapGenericType.MakeGenericType([string]) +$heap = [Activator]::CreateInstance($heapType) +$push = Find-Method $heapType 'Push' @([string], [double], [double], [double]) +$pop = Find-Method $heapType 'Pop' @() +Assert-True ($push -ne $null) 'BinaryMinHeap must expose Push(item, f, h, g).' +Assert-True ($pop -ne $null) 'BinaryMinHeap must expose Pop().' +$push.Invoke($heap, @('node-a', 8.0, 2.0, 1.0)) +$push.Invoke($heap, @('node-b', 8.0, 1.0, 1.0)) +$push.Invoke($heap, @('node-c', 8.0, 1.0, 3.0)) +$push.Invoke($heap, @('node-d', 7.0, 9.0, 0.0)) +$push.Invoke($heap, @('node-e', 8.0, 1.0, 3.0)) +Assert-Equal 'node-d' $pop.Invoke($heap, @()) 'Lower F must win.' +Assert-Equal 'node-c' $pop.Invoke($heap, @()) 'Equal F and H must prefer larger G.' +Assert-Equal 'node-e' $pop.Invoke($heap, @()) 'Equal F, H and G must preserve insertion order.' +Assert-Equal 'node-b' $pop.Invoke($heap, @()) 'Equal F must prefer lower H.' +Assert-Equal 'node-a' $pop.Invoke($heap, @()) 'Remaining item must be returned last.' +$firstDeterministicHeap = [Activator]::CreateInstance($heapType) +$secondDeterministicHeap = [Activator]::CreateInstance($heapType) +foreach ($deterministicHeap in @($firstDeterministicHeap, $secondDeterministicHeap)) { + $push.Invoke($deterministicHeap, @('repeat-a', 5.0, 2.0, 1.0)) + $push.Invoke($deterministicHeap, @('repeat-b', 5.0, 1.0, 1.0)) + $push.Invoke($deterministicHeap, @('repeat-c', 5.0, 1.0, 3.0)) +} +$firstDeterministicOrder = @($pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @())) -join ',' +$secondDeterministicOrder = @($pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @())) -join ',' +Assert-Equal $firstDeterministicOrder $secondDeterministicOrder 'Equivalent heap inputs must have deterministic pop order.' + +$calculatorType = $assembly.GetType($search + 'SearchCostCalculator', $true) +$calculator = [Activator]::CreateInstance($calculatorType) +$calculate = Find-Method $calculatorType 'Calculate' @([double], $directionType, [bool], [double], [double], [int], [double], $configurationType) +Assert-True ($calculate -ne $null) 'SearchCostCalculator must expose the planned equivalent-meter calculation.' +$costConfig = [Activator]::CreateInstance($configurationType) +$cost = $calculate.Invoke($calculator, @(2.0, $reverse, $true, 0.5, 1.0, 2, 0.25, $costConfig)) +Assert-Near 4.55 $cost 'Reverse, gear, curvature and clearance costs must be accumulated.' +$negativeWeightConfig = [Activator]::CreateInstance($configurationType) +$negativeWeightConfig.CurvatureMagnitudeWeight = -0.01 +Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $negativeWeightConfig)) } 'Negative cost weights must be rejected.' +$nonFiniteWeightConfig = [Activator]::CreateInstance($configurationType) +$nonFiniteWeightConfig.ClearanceCostWeight = [double]::NaN +Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $nonFiniteWeightConfig)) } 'Non-finite cost weights must be rejected.' + +$dijkstraType = $assembly.GetType($search + 'GridDijkstraHeuristic', $true) +$dijkstraConstructor = $dijkstraType.GetConstructor(@($map.GetType(), [int], [int])) +Assert-True ($dijkstraConstructor -ne $null) 'GridDijkstraHeuristic must expose the planned grid constructor.' +$getCost = Find-Method $dijkstraType 'GetCost' @([int], [int]) +Assert-True ($getCost -ne $null) 'GridDijkstraHeuristic must expose GetCost(row, col).' +$openHeuristic = $dijkstraConstructor.Invoke(@($map, 2, 2)) +$expectedDiagonalCost = 2.0 * [Math]::Sqrt(2.0) * $map.ResolutionMeters +Assert-Near $expectedDiagonalCost $getCost.Invoke($openHeuristic, @(0, 0)) 'Open-grid diagonal movement must cost sqrt(2) per cell.' +$cornerBlockedMap = New-CornerBlockedMap +$blockedHeuristic = $dijkstraConstructor.Invoke(@($cornerBlockedMap, 1, 1)) +Assert-True ([double]::IsPositiveInfinity($getCost.Invoke($blockedHeuristic, @(0, 0)))) 'Diagonal corner cutting through two occupied orthogonal cells must be forbidden.' + +$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $true) +$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $true) +$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, + @([Threading.CancellationToken], [TimeSpan]), $null) +Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' +$tryCreateDijkstra = Find-NonPublicStaticMethod $dijkstraType 'TryCreate' @($map.GetType(), [int], [int], $operationBudgetType, + $dijkstraType.MakeByRefType(), $operationStopReasonType.MakeByRefType()) +Assert-True ($tryCreateDijkstra -ne $null) 'GridDijkstraHeuristic must expose an internal budget-aware TryCreate method.' +$expiredBudget = $budgetConstructor.Invoke(@([Threading.CancellationToken]::None, [TimeSpan]::Zero)) +$dijkstraArguments = [object[]]@($map, 2, 2, $expiredBudget, $null, $null) +$dijkstraCreated = $tryCreateDijkstra.Invoke($null, $dijkstraArguments) +Assert-False $dijkstraCreated 'An expired total budget must stop Dijkstra construction.' +Assert-Null $dijkstraArguments[4] 'Stopped Dijkstra construction must not publish a partial heuristic.' +Assert-Equal 'TimedOut' $dijkstraArguments[5].ToString() 'Expired Dijkstra construction must report timeout.' + +$largeBounds = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true), + @([single]0, [single]40000, [single]0, [single]20000)) +$largeMapRequest = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true)) +$largeMapRequest.Bounds = $largeBounds +$largeMapRequest.ResolutionMm = [single]20 +$largeMapRequest.AllowExplicitEmptyMap = $true +$largeMap = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory', $true)).Create($largeMapRequest) +Assert-True $largeMap.Succeeded 'Large empty map for Dijkstra cancellation must be created before cancellation starts.' +$midDijkstraCancellation = New-Object Threading.CancellationTokenSource +$midDijkstraBudget = $budgetConstructor.Invoke(@($midDijkstraCancellation.Token, [TimeSpan]::FromSeconds(2))) +$midDijkstraCancellation.CancelAfter(1) +$midDijkstraTimer = [Diagnostics.Stopwatch]::StartNew() +$midDijkstraArguments = [object[]]@($largeMap.Map, 500, 1000, $midDijkstraBudget, $null, $null) +$midDijkstraCreated = $tryCreateDijkstra.Invoke($null, $midDijkstraArguments) +$midDijkstraTimer.Stop() +Assert-False $midDijkstraCreated 'Cancellation during Dijkstra initialization must stop construction.' +Assert-Null $midDijkstraArguments[4] 'Mid-Dijkstra cancellation must not publish a partial heuristic.' +Assert-Equal 'Cancelled' $midDijkstraArguments[5].ToString() 'Mid-Dijkstra cancellation must retain cancellation status.' +Assert-True ($midDijkstraTimer.Elapsed -lt [TimeSpan]::FromSeconds(2)) 'Dijkstra cancellation must return within the configured response bound.' + +Write-Output 'Coarse path search primitive checks passed.' + +$nodeType = $assembly.GetType($search + 'HybridAStarNode', $true) +$nodeKeyType = $assembly.GetType($search + 'HybridAStarNodeKey', $true) +$searchType = $assembly.GetType($search + 'HybridAStarSearch', $true) +$searchResultType = $assembly.GetType($search + 'HybridAStarSearchResult', $true) +$searchMethod = Find-Method $searchType 'Search' @($requestType, [Threading.CancellationToken]) +Assert-True ($searchMethod -ne $null) 'HybridAStarSearch must expose Search(request, cancellationToken).' +Assert-True ($searchResultType.GetProperty('Status') -ne $null) 'Search result must expose planning status.' +Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) 'Search result must expose its original termination reason.' +Assert-True ($searchResultType.GetProperty('SuccessNodeIndex') -ne $null) 'Search result must expose the successful node index.' +Assert-True ($searchResultType.GetProperty('ReopenedNodeCount') -ne $null) 'Search result must expose reopened node count.' +Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entry count.' +Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak count.' +Assert-True ($nodeType.GetProperty('ParentNodeIndex') -ne $null) 'Search node must expose its parent node index.' +Assert-True ($nodeKeyType.GetProperty('HeadingIndex') -ne $null) 'Search node key must contain a heading index.' +Assert-True ($nodeKeyType.GetProperty('Direction') -ne $null) 'Search node key must contain a travel direction.' +Assert-True ($nodeKeyType.GetProperty('CurvatureLevelIndex') -ne $null) 'Search node key must contain a curvature level index.' + +$goalCandidateProperty = $nodeType.GetProperty('IsGoalCandidate') +Assert-True ($goalCandidateProperty -ne $null) 'Search node must mark a goal-truncated candidate explicitly.' +$nodeConstructor = $nodeType.GetConstructor(@([int], $nodeKeyType, $poseType, [int], $primitiveType, [double], [double], [double], [bool])) +Assert-True ($nodeConstructor -ne $null) 'Search node must accept the explicit goal-candidate marker.' +$admissionMethod = $searchType.GetMethod('ShouldEnqueueSuccessor', [Reflection.BindingFlags]'NonPublic,Static') +Assert-True ($admissionMethod -ne $null) 'Search must expose its private successor admission policy for reflection regression coverage.' + +$candidateStart = [Activator]::CreateInstance($poseType, @(1.000, 1.000, 0.0)) +$candidateGoal = [Activator]::CreateInstance($poseType, @(1.024, 1.000, 0.0)) +$candidatePoints = [Array]::CreateInstance($poseType, 1) +$candidatePoints.SetValue($candidateGoal, 0) +$candidatePrimitive = [Activator]::CreateInstance($primitiveType, @($candidateStart, $forward, 0.0, 0.024, $candidatePoints, [double[]]@(1.0), $true)) +$sharedKey = [Activator]::CreateInstance($nodeKeyType, @(20, 20, 0, $forward, 2)) +$normalNode = $nodeConstructor.Invoke(@(10, $sharedKey, $candidateStart, 0, $null, 0.0, 0.250, 0.0, $false)) +$goalCandidateNode = $nodeConstructor.Invoke(@(11, $sharedKey, $candidateGoal, 10, $candidatePrimitive, 0.0, 0.300, 0.0, $true)) +$dominanceConfiguration = [Activator]::CreateInstance($configurationType) +$dominanceConfiguration.GoalPositionToleranceMeters = 0.001 +$dominanceConfiguration.GoalHeadingToleranceRadians = 0.001 +Assert-True $candidatePrimitive.IsGoalTruncation 'Regression setup must use a goal-truncated motion primitive.' +Assert-True $normalNode.Key.Equals($goalCandidateNode.Key) 'Regression setup must share one discrete state key.' +Assert-False $isSatisfied.Invoke($null, @($normalNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Lower-G normal node must remain outside the continuous goal tolerance.' +Assert-True $isSatisfied.Invoke($null, @($goalCandidateNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Goal candidate must satisfy the continuous goal tolerance.' +Assert-False $admissionMethod.Invoke($null, @($normalNode, 0.250)) 'Equal-cost normal state must be dominated by the best same-key label.' +Assert-True $admissionMethod.Invoke($null, @($goalCandidateNode, 0.250)) 'Goal candidate must enter Open List even when a lower-G normal state has the same key.' + +function New-SearchRequest([object]$Goal, [object]$GoalDirection, [bool]$AllowReverse) { + $searchRequest = [Activator]::CreateInstance($requestType) + $searchRequest.Map = $map + $searchRequest.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) + $searchRequest.Goal = $Goal + $searchRequest.Vehicle = $vehicle + $searchConfiguration = [Activator]::CreateInstance($configurationType) + $searchConfiguration.PrimitiveLengthMeters = 0.50 + $searchConfiguration.IntegrationStepMeters = 0.05 + $searchConfiguration.MaximumCollisionCheckStepMeters = 0.025 + $searchConfiguration.GoalPositionToleranceMeters = 0.001 + $searchConfiguration.GoalHeadingToleranceRadians = 0.001 + $searchConfiguration.CurvatureLevelCount = 5 + $searchConfiguration.MaximumExpandedNodes = 10000 + $searchConfiguration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) + $searchConfiguration.AllowReverse = $AllowReverse + $searchRequest.Configuration = $searchConfiguration + $searchRequest.GoalDirection = $GoalDirection + return $searchRequest +} + +$searcher = [Activator]::CreateInstance($searchType) +$searchGoal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) +$searchRequest = New-SearchRequest $searchGoal $forwardOnly $false +$searchResult = $searchMethod.Invoke($searcher, @($searchRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $searchResult.Status.ToString() 'Empty map forward search must succeed.' +Assert-True ($null -ne $searchResult.SuccessNodeIndex) 'Success must be reported only with a popped goal candidate node.' +Assert-True ($searchResult.ExpandedNodeCount -gt 0) 'A successful search must expand the selected goal candidate.' + +$atGoal = New-SearchRequest $searchRequest.Start $forwardOnly $false +$atGoalResult = $searchMethod.Invoke($searcher, @($atGoal, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $atGoalResult.Status.ToString() 'A zero-length goal candidate must enter and leave the open list successfully.' +Assert-True ($null -ne $atGoalResult.SuccessNodeIndex) 'A zero-length goal candidate must have a node index.' + +$cancelledRequest = New-SearchRequest $searchGoal $forwardOnly $false +$cancellationSource = New-Object Threading.CancellationTokenSource +$cancellationSource.Cancel() +$cancelledResult = $searchMethod.Invoke($searcher, @($cancelledRequest, $cancellationSource.Token)) +Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Cancellation must be observed before node expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) 'Cancelled search must retain a non-empty reason.' + +$limitedRequest = New-SearchRequest $searchGoal $forwardOnly $false +$limitedRequest.Configuration.MaximumExpandedNodes = 0 +$limitedResult = $searchMethod.Invoke($searcher, @($limitedRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchNodeLimitExceeded' $limitedResult.Status.ToString() 'Node limit must be checked before expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) 'Node-limited search must retain a non-empty reason.' + +$timedOutRequest = New-SearchRequest $searchGoal $forwardOnly $false +$timedOutRequest.Configuration.SearchTimeout = [TimeSpan]::Zero +$timedOutResult = $searchMethod.Invoke($searcher, @($timedOutRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchTimeout' $timedOutResult.Status.ToString() 'Timeout must be checked before expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) 'Timed-out search must retain a non-empty reason.' +Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) 'Cancelled and node-limited searches must retain different reasons.' +Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) 'Node-limited and timed-out searches must retain different reasons.' + +$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | + Where-Object { + $_.Name -eq 'Search' -and + $_.GetParameters().Length -eq 2 -and + $_.GetParameters()[1].ParameterType -eq $operationBudgetType + } | + Select-Object -First 1 +Assert-True ($internalSearchMethod -ne $null) 'Search must retain its internal shared-budget overload.' +$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) +Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() 'A missing internal budget must be mapped to InternalError.' +Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) 'Internal search errors must retain the exception type.' + +Write-Output 'Coarse path Hybrid A star search checks passed.' diff --git a/.task8-sweep/tests/verify_coarse_path_ui.ps1 b/.task8-sweep/tests/verify_coarse_path_ui.ps1 new file mode 100644 index 0000000..58b2d00 --- /dev/null +++ b/.task8-sweep/tests/verify_coarse_path_ui.ps1 @@ -0,0 +1,122 @@ +$ErrorActionPreference = 'Stop' + +function Assert-True([bool]$condition, [string]$message) { + if (-not $condition) { throw $message } +} + +function Assert-Match([string]$content, [string]$pattern, [string]$message) { + Assert-True ($content -match $pattern) $message +} + +function Assert-NotMatch([string]$content, [string]$pattern, [string]$message) { + Assert-True ($content -notmatch $pattern) $message +} + +$sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\Test\MovementTest.CoarsePathTest.cs' +Assert-True (Test-Path -LiteralPath $sourcePath) 'P1 coarse-path UI movement-test source must exist.' +$source = Get-Content -Raw -Encoding UTF8 $sourcePath + +Assert-Match $source 'class\s+CoarsePathPlanningTest\s*:\s*MovementTest' 'The UI entry must inherit MovementTest.' +Assert-Match $source 'namespace\s+MultiWheelC\s*;' 'MovementTest entries must use the project discovery namespace.' +Assert-Match $source 'CoarsePathPlanningService' 'The UI must keep one shared planning service for map-cache reuse.' +Assert-Match $source 'Task\.Run\s*\(' 'Planning must run in a background Task.' +Assert-Match $source 'CancellationTokenSource' 'The UI must retain a cancellation source for its active task.' +Assert-Match $source 'override\s+void\s+TestStop\s*\(' 'The UI must implement TestStop cancellation.' +Assert-Match $source 'CoarsePathScenarioFactory\.Create\s*\(' 'The UI must expose fixed scenario entries through the factory.' +Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' +Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' +Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' +Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' +Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' +Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' +Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' +Assert-Match $source 'UI\.GetInput\s*\(' 'The manual demo must accept user input.' +Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' 'The manual UI must read a finite positive timeout.' +Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' 'The manual UI must apply the timeout to the current job.' +Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' 'The manual timeout must convert seconds to TimeSpan.' +Assert-Match $source 'timeoutSeconds\s*<=\s*0' 'The manual timeout must reject zero and negative values.' +Assert-Match $source 'getCartLocation\s*\(' 'The manual demo must read the AMR body-center pose.' +Assert-Match $source 'DrawLegend\s*\(' 'The painter output must include a visual legend.' +Assert-Match $source 'PlanningGridMap' 'The painter output must consume the planning grid result.' +Assert-Match $source '\.IsOccupied\s*\(' 'The painter output must draw occupied grid cells.' +Assert-Match $source 'ResolutionMm' 'The painter output must draw the grid at its true resolution.' +Assert-Match $source 'SnapshotId' 'The painter output must show the map snapshot identity.' +Assert-Match $source 'IsGearSwitchPoint' 'The painter output must highlight gear-switch points.' +Assert-Match $source 'GoalPositionToleranceMeters' 'The painter output must draw goal tolerance.' +Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) 'The status layer and Toast must both show path-search elapsed time.' +Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' 'The failure Toast must include the termination reason.' +Assert-Match $source 'ExpandedNodeCount' 'The status layer must show expanded-node statistics.' +Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' 'The status layer must show the effective turning radius.' +Assert-NotMatch $source 'PlanningMapFactory' 'Movement tests must not build maps directly; use CoarsePathPlanningService.' +Assert-NotMatch $source '\.Result\b' 'The UI must not block on Task.Result.' +Assert-NotMatch $source '\.Wait\s*\(' 'The UI must not block the movement-test thread.' + +$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') +Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' +$runnerSource = $source.Substring($runnerStart) +Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' +Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' +Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' +Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' +Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' +Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' +$inputFailureStart = $runnerSource.IndexOf('internal static void ShowInputFailure') +$inputFailureEnd = $runnerSource.IndexOf('private static void Finish', $inputFailureStart) +Assert-True ($inputFailureStart -ge 0 -and $inputFailureEnd -gt $inputFailureStart) 'Input failure renderer must be isolated before completion handling.' +$inputFailureSource = $runnerSource.Substring($inputFailureStart, $inputFailureEnd - $inputFailureStart) +Assert-Match $inputFailureSource 'Painter\.DrawText\s*\(' 'Invalid AMR input must be shown on the Painter status layer.' + +$readmePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\README.md' +Assert-True (Test-Path -LiteralPath $readmePath) 'CoarsePath README must exist beside its module.' +$readme = Get-Content -Raw -Encoding UTF8 $readmePath +foreach ($requiredText in @( + 'CoarsePathPlanningV1', + 'CoarsePathPlanningTest', + 'CreateManualGoalDemo', + 'getCartLocation', + 'mm', + 'deg', + 'rad', + 'CancellationTokenSource', + 'TestStop', + 'NoFeasiblePath', + 'IsGearSwitchPoint')) { + Assert-True ($readme.Contains($requiredText)) "P1 README must document $requiredText." +} + +$readmeStructure = @( + 'File Structure', + 'Planning Data Flow', + 'Build Status and Stop', + 'Coordinates and Units', + 'Minimal Call Example', + 'Cache and SourceVersion', + 'Detailed Usage Guide', + 'P1 Manual Tests and Visualization', + 'Common Errors', + 'First-Version Limits', + 'CoarsePathPlanningService.Plan(job, cancellationToken)', + 'CoarsePathPlanningJob', + 'PlanningGridMap', + 'SourceVersion', + 'CoarsePathPlanningV1', + 'CancellationTokenSource', + 'NoFeasiblePath', + 'IsGearSwitchPoint', + 'CreateManualObstacleDemo', + 'ManualCoarsePathObstacle', + 'manual-user-input', + '0-20', + 'PathSearchElapsed', + '1.20 m', + 'Reeds-Shepp', + 'AxisAlignedRectangle', + 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', + 'DetectionHeadingRadians', + '../Map/README.md' +) +foreach ($requiredText in $readmeStructure) { + Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." +} + +Write-Output 'Coarse path P1 UI source checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_algorithm_input.ps1 b/.task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_algorithm_input.ps1 rename to .task8-sweep/tests/verify_path_smoothing_algorithm_input.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_bezier.ps1 b/.task8-sweep/tests/verify_path_smoothing_bezier.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_bezier.ps1 rename to .task8-sweep/tests/verify_path_smoothing_bezier.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_bspline.ps1 b/.task8-sweep/tests/verify_path_smoothing_bspline.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_bspline.ps1 rename to .task8-sweep/tests/verify_path_smoothing_bspline.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 b/.task8-sweep/tests/verify_path_smoothing_comparison.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 rename to .task8-sweep/tests/verify_path_smoothing_comparison.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 b/.task8-sweep/tests/verify_path_smoothing_contracts.ps1 similarity index 99% rename from ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 rename to .task8-sweep/tests/verify_path_smoothing_contracts.ps1 index 6838d1f..75726c4 100644 --- a/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 +++ b/.task8-sweep/tests/verify_path_smoothing_contracts.ps1 @@ -71,7 +71,7 @@ Assert-Equal 'Improved,RetainedOriginal' ([string]::Join(',', [Enum]::GetNames($ Assert-Equal 'None,WindowUnavailable,CandidateGenerationFailed,Collision,InsufficientClearance,CurvatureExceeded,CurvatureOvershoot,DeviationExceeded,InsufficientImprovement,VariationCostRegression,GlobalValidationRollback' ([string]::Join(',', [Enum]::GetNames($regionFailureType))) 'Local G2 region failure reasons must be stable.' $configuration = [Activator]::CreateInstance($configurationType) -Assert-Near 0.05 $configuration.OutputSpacingMeters 'Default output spacing must be 0.05 m.' +Assert-Near 0.025 $configuration.OutputSpacingMeters 'Default output spacing must be 0.025 m.' Assert-Near 0.025 $configuration.MaximumCollisionCheckStepMeters 'Default collision step must be 0.025 m.' Assert-Near 0.02 $configuration.MinimumClearanceReserveMeters 'Default clearance reserve must be 0.02 m.' Assert-Near 1.0 $configuration.SmoothingStrength 'Default smoothing strength must be 1.0.' @@ -319,7 +319,7 @@ Assert-Near 0.05 $request.Vehicle.SafetyMarginMeters 'Request must snapshot vehi Assert-Near (1.0 / 1.20) $request.Vehicle.MaximumCurvaturePerMeter 'Request must snapshot nullable vehicle curvature.' Assert-True ($null -eq $request.Vehicle.MinimumTurningRadiusMeters) 'Request must snapshot nullable vehicle turning radius.' Assert-Equal 'CubicBSpline' $request.Configuration.Method.ToString() 'Request must snapshot smoothing method.' -Assert-Near 0.05 $request.Configuration.OutputSpacingMeters 'Request must snapshot common configuration.' +Assert-Near 0.025 $request.Configuration.OutputSpacingMeters 'Request must snapshot common configuration.' Assert-Near 0.025 $request.Configuration.MaximumCollisionCheckStepMeters 'Request must snapshot collision configuration.' Assert-Near 0.02 $request.Configuration.MinimumClearanceReserveMeters 'Request must snapshot clearance configuration.' Assert-Near 1.0 $request.Configuration.SmoothingStrength 'Request must snapshot smoothing strength.' diff --git a/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 b/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 new file mode 100644 index 0000000..b8538e1 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_documentation.ps1 @@ -0,0 +1,65 @@ +$ErrorActionPreference = 'Stop' + +$root = Join-Path $PSScriptRoot '..' +$readmePath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\README.md' +$demoPath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\Test\PathSmoothingComparisonDemo.cs' +$runnerPath = Join-Path $PSScriptRoot 'run_path_smoothing_comparison.ps1' + +function Assert-True($actual, [string]$message) { + if (-not $actual) { throw $message } +} + +Assert-True (Test-Path -LiteralPath $readmePath) 'Path smoothing README is missing.' +Assert-True (Test-Path -LiteralPath $demoPath) 'Path smoothing comparison demo is missing.' +Assert-True (Test-Path -LiteralPath $runnerPath) 'Path smoothing comparison batch runner is missing.' + +$readme = Get-Content -Raw -Encoding UTF8 $readmePath +foreach ($section in @( + '## Units and coordinates', + '## Facade usage', + '## Status handling and fallback', + '## Fixture freshness', + '## IEEE colors, fonts, and Windows PNG', + '## SQP boundary', + '## Output files')) { + Assert-True $readme.Contains($section) "README must contain section: $section" +} +foreach ($requiredText in @( + 'meters', + 'radians', + 'PathSmoothingService', + 'FallbackToCoarsePath', + 'SimSun', + 'Times New Roman', + '600 dpi', + 'discrete samples', + '01-coarse-path-overview', + '02-all-paths-comparison', + '03-cubic-bspline-overview', + '04-local-cubic-bezier-overview', + '05-piecewise-quintic-overview', + '06-curvature-comparison', + 'PDF/EPS')) { + Assert-True $readme.Contains($requiredText) "README must document: $requiredText" +} + +$demo = Get-Content -Raw -Encoding UTF8 $demoPath +foreach ($requiredText in @( + 'PathSmoothingComparisonDemo', + 'PathSmoothingService', + 'PathSmoothingComparisonService', + 'SmoothingReportExporter', + 'SmoothingScenarioFactory', + 'PathSmoothingStatus.Success', + 'PathSmoothingStatus.FallbackToCoarsePath')) { + Assert-True $demo.Contains($requiredText) "Demo must use: $requiredText" +} + +$runner = Get-Content -Raw -Encoding UTF8 $runnerPath +Assert-True ($runner -match '\[switch\]\$FixtureOnly') 'Batch runner must offer -FixtureOnly.' +Assert-True $runner.Contains('obj\path_smoothing_reports') 'Batch runner must write reports below ClumsyPilot/obj/path_smoothing_reports.' +Assert-True $runner.Contains('--export-fixtures') 'Batch runner must export all eight fast fixtures by default.' +Assert-True $runner.Contains('if (-not $FixtureOnly)') 'Batch runner must make end-to-end cases optional.' +Assert-True ($runner -notmatch 'ParkrobTrajplanner\\PathSmoothing\\.*\.(svg|png|csv)') 'Batch runner must never write report files below source directories.' + +Write-Output 'Path smoothing documentation checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 b/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 new file mode 100644 index 0000000..ec883c4 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_fixtures.ps1 @@ -0,0 +1,119 @@ +param( + [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json')) + +$ErrorActionPreference = 'Stop' +$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} +function Assert-ThrowsMatching([scriptblock]$Action, [string]$ExpectedPattern, [string]$Message) { + try { + & $Action + } + catch { + $exception = $_.Exception + while ($null -ne $exception.InnerException) { $exception = $exception.InnerException } + if ($exception.Message -match $ExpectedPattern) { return } + throw "$Message ExpectedPattern=$ExpectedPattern Actual=$($exception.Message)" + } + throw "$Message Expected an exception." +} +function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.' +$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison.' +$loaderType = Get-RequiredType ($root + 'SmoothingScenarioFixtureLoader') +$factoryType = Get-RequiredType ($root + 'SmoothingScenarioFactory') +$fixtureType = Get-RequiredType ($root + 'SmoothingScenarioFixture') +$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') +$preprocessorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing.PathSmoothingPreprocessor' + +$loadMethod = $loaderType.GetMethod('LoadAndVerify', [Type[]]@([string])) +Assert-True ($null -ne $loadMethod) 'Fixture loader must expose LoadAndVerify(string).' +$fixtureRequestsMethod = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) +Assert-True ($null -ne $fixtureRequestsMethod) 'Scenario factory must expose CreateFixtureRequests(string).' +$generateMethod = $assembly.GetType($root + 'SmoothingFixtureGenerator', $true).GetMethod('Generate', [Type[]]@([string], [bool])) +Assert-True ($null -ne $generateMethod) 'Fixture generator must expose Generate(string, bool).' +$tryPrepareMethod = $preprocessorType.GetMethod('TryPrepare') +Assert-True ($null -ne $tryPrepareMethod) 'Fixture paths must be checked through PathSmoothingPreprocessor.TryPrepare.' + +$fixtures = $loadMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) +$fixtureDocument = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json +$expected = @( + 'straight', 'single-turn', 's-bend', 'large-heading-change', + 'rectangle-detour', 'multi-obstacle-detour', + 'narrow-corridor', 'forward-reverse-switch') +Assert-Equal $expected.Count $fixtures.Count 'Fixture loader must return exactly eight fast fixtures.' + +$actualIds = @($fixtures | ForEach-Object { $_.Id }) +Assert-Equal ($expected -join ',') ($actualIds -join ',') 'Fixture IDs must be stable and in documented order.' +Assert-Equal $actualIds.Count (@($actualIds | Select-Object -Unique).Count) 'Fixture IDs must be unique.' +foreach ($fixture in $fixtures) { + Assert-True ($fixture -is $fixtureType) 'Loader must return immutable smoothing fixture values.' + Assert-True ($fixture.FixtureVersion -gt 0) "Fixture $($fixture.Id) must carry a positive version." + Assert-True $fixture.IsConfigurationFingerprintCurrent "Fixture $($fixture.Id) must match its stored configuration fingerprint." + Assert-True ($fixture.ConfigurationFingerprint -match '^sha256:[0-9a-f]{64}$') "Fixture $($fixture.Id) must expose a lowercase SHA-256 fingerprint." + Assert-True ($fixture.Path.Count -gt 1) "Fixture $($fixture.Id) must include a coarse path." + Assert-True ($fixture.Segments.Count -gt 0) "Fixture $($fixture.Id) must include direction segments." + foreach ($segment in $fixture.Segments) { + Assert-True ($segment.StartIndex -ge 0 -and $segment.EndIndex -lt $fixture.Path.Count -and $segment.EndIndex -ge $segment.StartIndex) "Fixture $($fixture.Id) must retain valid segment coverage." + } +} +foreach ($record in $fixtureDocument.scenarios) { + Assert-True ($null -ne $record.planningConfiguration) "Fixture $($record.id) must retain the planning configuration that produced its coarse path." +} + +$requests = $fixtureRequestsMethod.Invoke($null, @((Resolve-Path $FixturePath).Path)) +Assert-Equal 8 $requests.Count 'Fast fixtures must create exactly eight comparison requests without Hybrid A*.' +foreach ($request in $requests) { + Assert-True ($request -is $comparisonRequestType) 'Fast fixture factory must create comparison requests.' + $prepareArguments = [object[]]@($request.SmoothingRequest, $null, $null) + $prepared = $tryPrepareMethod.Invoke([Activator]::CreateInstance($preprocessorType), $prepareArguments) + Assert-True $prepared "Fixture path must satisfy the PathSmoothingPreprocessor input contract. Reason=$($prepareArguments[2])" +} + +$loaderSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingScenarioFixtureLoader.cs') +Assert-True (-not $loaderSource.Contains('HybridAStarPlanner')) 'Fixture-only loading must not reference HybridAStarPlanner.' +$generatorSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\SmoothingFixtureGenerator.cs') +Assert-True $generatorSource.Contains('CoarsePathPlanningService') 'Fixture generation must snapshot actual successful coarse-path planning outputs.' +Assert-True $generatorSource.Contains('CoarsePathScenarioFactory') 'Fixture generation must use the established coarse-path scenarios where available.' + +function Write-CorruptedFixture([scriptblock]$Mutate) { + $temporaryPath = [IO.Path]::GetTempFileName() + $document = Get-Content -Raw -Encoding UTF8 $FixturePath | ConvertFrom-Json + & $Mutate $document + [IO.File]::WriteAllText($temporaryPath, ($document | ConvertTo-Json -Depth 16), [Text.UTF8Encoding]::new($false)) + return $temporaryPath +} + +$coverageFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endIndex = 1 } +$directionFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].direction = 1 } +$switchFixture = Write-CorruptedFixture { param($document) $document.scenarios[0].segments[0].endsAtGearSwitch = $true } +try { + Assert-ThrowsMatching { $loadMethod.Invoke($null, @($coverageFixture)) } 'Fixture path contract' 'Loader must reject segment gaps before accepting the fingerprint.' + Assert-ThrowsMatching { $loadMethod.Invoke($null, @($directionFixture)) } 'Fixture path contract' 'Loader must reject segment direction mismatches before accepting the fingerprint.' + Assert-ThrowsMatching { $loadMethod.Invoke($null, @($switchFixture)) } 'Fixture path contract' 'Loader must reject illegal gear-switch topology before accepting the fingerprint.' +} +finally { + foreach ($temporaryPath in @($coverageFixture, $directionFixture, $switchFixture)) { + if ($temporaryPath -and [IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } + } +} + +$generationPath = [IO.Path]::GetTempFileName() +try { + Assert-ThrowsMatching { $generateMethod.Invoke($null, @($generationPath, $false)) } '.+' 'Fixture generation must refuse to overwrite an existing target.' + $generateMethod.Invoke($null, @($generationPath, $true)) + $generatedFixtures = $loadMethod.Invoke($null, @($generationPath)) + Assert-Equal 8 $generatedFixtures.Count 'Generated fixture data must remain loadable and retain all scenarios.' +} +finally { + if ([IO.File]::Exists($generationPath)) { [IO.File]::Delete($generationPath) } +} + +Write-Output 'Path smoothing fixture checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 b/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 new file mode 100644 index 0000000..8aee66e --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_geometry.ps1 @@ -0,0 +1,642 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { + throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" + } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Throws([scriptblock]$Action, [string]$Message) { + try { + & $Action + } + catch { + return + } + throw $Message +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +function New-GeometryPoint( + [double]$X, + [double]$Y, + [double]$ArcLength, + [double]$Heading, + [double]$UnwrappedHeading, + [bool]$IsGearSwitch = $false) { + return [Activator]::CreateInstance($pointType, @( + $X, $Y, $ArcLength, $Heading, $UnwrappedHeading, + [double]1.0, $IsGearSwitch, $anchor)) +} + +function New-DirectionSegment( + [int]$Index, + $Direction, + [object[]]$Points, + [bool]$StartsAtGearSwitch = $false, + [bool]$EndsAtGearSwitch = $false) { + $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) + for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { + $typedPoints.SetValue($Points[$pointIndex], $pointIndex) + } + + return [Activator]::CreateInstance($segmentType, @( + $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch)) +} + +function New-DirectionSegmentWithStartCurvature( + [int]$Index, + $Direction, + [double]$StartVehicleCurvature, + [object[]]$Points, + [bool]$StartsAtGearSwitch = $false, + [bool]$EndsAtGearSwitch = $false) { + $typedPoints = [Array]::CreateInstance($pointType, $Points.Count) + for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) { + $typedPoints.SetValue($Points[$pointIndex], $pointIndex) + } + + return [Activator]::CreateInstance($segmentType, @( + $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch, $StartVehicleCurvature)) +} + +function New-CoarsePoint( + [double]$X, + [double]$Y, + [double]$ArcLength, + $Direction, + [bool]$IsGearSwitch = $false) { + return [Activator]::CreateInstance($coarsePointType, @( + $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction, + [double]0.0, [double]1.0, $IsGearSwitch, $coarseAnchor)) +} + +function Invoke-Analysis([object[]]$Segments, [double]$Spacing = 0.05) { + $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) + for ($index = 0; $index -lt $Segments.Count; $index++) { + $typedSegments.SetValue($Segments[$index], $index) + } + + $arguments = [object[]]@($typedSegments, $Spacing, $null, $null) + $accepted = $analyzeMethod.Invoke($analyzer, $arguments) + $description = [string]::Join(',', @($Segments | ForEach-Object { + "index=$($_.SegmentIndex);direction=$($_.Direction);points=$($_.Points.Count)" + })) + Assert-True $accepted ("Geometry analysis must accept the analytic candidate. Reason=" + $arguments[3] + '; Segments=' + $description) + Assert-True ($null -ne $arguments[2]) 'Successful geometry analysis must return PathGeometryAnalysis.' + return $arguments[2] +} + +function Assert-AnalysisRejected([object[]]$Segments, [string]$Message) { + $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) + for ($segmentIndex = 0; $segmentIndex -lt $Segments.Count; $segmentIndex++) { + $typedSegments.SetValue($Segments[$segmentIndex], $segmentIndex) + } + + $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) + $accepted = $analyzeMethod.Invoke($analyzer, $arguments) + Assert-True (-not $accepted) ($Message + '; Reason=' + $arguments[3]) +} + +function Invoke-RejectedAnalysis([object[]]$Segments, [string]$Message) { + $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) + for ($index = 0; $index -lt $Segments.Count; $index++) { + $typedSegments.SetValue($Segments[$index], $index) + } + + $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null) + $accepted = $analyzeMethod.Invoke($analyzer, $arguments) + Assert-False $accepted ($Message + '; Reason=' + $arguments[3]) +} + +function New-CoarsePathPoint( + [double]$X, + [double]$Y, + [double]$ArcLength, + $Direction, + [bool]$IsGearSwitch = $false, + [string]$SourceName = 'MotionPrimitive', + [double]$Heading = 0.0, + [double]$VehicleCurvature = 0.0) { + return [Activator]::CreateInstance($coarsePointType, @( + $X, $Y, $Heading, $Heading, $ArcLength, + $Direction, $VehicleCurvature, [double]1.0, $IsGearSwitch, + [Enum]::Parse($coarsePointSourceType, $SourceName))) +} + +function New-EmptyGeometryMap { + $mapRequest = [Activator]::CreateInstance($mapRequestType) + $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $mapRequest.ResolutionMm = [single]50 + $mapRequest.AllowExplicitEmptyMap = $true + $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map + Assert-True ($null -ne $map) 'Geometry test must create an explicit empty planning map.' + return $map +} + +function Invoke-GeometryValidation($Analysis, [object[]]$Segments, $Vehicle) { + $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count) + for ($index = 0; $index -lt $Segments.Count; $index++) { + $typedSegments.SetValue($Segments[$index], $index) + } + + $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]](, $typedSegments)) + $arguments = [object[]]@( + $Analysis.Path, $Analysis.Segments, $preparedPath, (New-EmptyGeometryMap), $Vehicle, [double]0.05, + $null, [double]0.0, $null) + $accepted = $validateMethod.Invoke($validator, $arguments) + return [pscustomobject]@{ Accepted = $accepted; Reason = $arguments[8] } +} + +function New-CircularDirectionSegment( + $Direction, + [double]$Curvature, + [int]$Intervals, + [double]$ChordLength) { + $radius = 1.0 / $Curvature + $headingStep = 2.0 * [Math]::Asin($Curvature * $ChordLength / 2.0) + $points = New-Object System.Collections.Generic.List[object] + for ($index = 0; $index -le $Intervals; $index++) { + $theta = $headingStep * $index + $heading = if ($Direction.ToString() -eq 'Forward') { $theta } else { $theta + [Math]::PI } + [void]$points.Add((New-GeometryPoint ` + (1.0 + $radius * [Math]::Sin($theta)) ` + (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` + ($index * $ChordLength) $heading $heading)) + } + return New-DirectionSegment 0 $Direction $points.ToArray() +} + +function Get-OldPolylineCurvatureMaximum($Analysis) { + $maximum = 0.0 + $path = $Analysis.Path + for ($index = 0; $index -lt $path.Count; $index++) { + if ($path.Count -eq 1) { + $curvature = 0.0 + } + elseif ($index -eq 0) { + $curvature = ($path[1].UnwrappedHeading - $path[0].UnwrappedHeading) / + ($path[1].ArcLength - $path[0].ArcLength) + } + elseif ($index -eq $path.Count - 1) { + $curvature = ($path[$index].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / + ($path[$index].ArcLength - $path[$index - 1].ArcLength) + } + else { + $curvature = ($path[$index + 1].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) / + ($path[$index + 1].ArcLength - $path[$index - 1].ArcLength) + } + $maximum = [Math]::Max($maximum, [Math]::Abs($curvature)) + } + return $maximum +} + +function Get-QuinticAnalyticCurvatureMaximum( + [double]$C2, + [double]$C3, + [double]$C4, + [double]$C5, + [int]$ReferenceSamples = 20000) { + $maximum = 0.0 + for ($index = 0; $index -lt $ReferenceSamples; $index++) { + $t = $index / [double]($ReferenceSamples - 1) + $firstDerivative = 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + + 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t + $secondDerivative = 2.0 * $C2 + 6.0 * $C3 * $t + + 12.0 * $C4 * $t * $t + 20.0 * $C5 * $t * $t * $t + $curvature = [Math]::Abs($secondDerivative / [Math]::Pow(1.0 + $firstDerivative * $firstDerivative, 1.5)) + $maximum = [Math]::Max($maximum, $curvature) + } + return $maximum +} + +function New-QuinticDirectionSegment( + [double]$C2, + [double]$C3, + [double]$C4, + [double]$C5, + [double]$DistributionPower, + [int]$Samples = 400) { + $points = New-Object System.Collections.Generic.List[object] + $arcLength = 0.0 + $previousX = 0.0 + $previousY = 0.0 + for ($index = 0; $index -lt $Samples; $index++) { + $t = [Math]::Pow($index / [double]($Samples - 1), $DistributionPower) + $x = 1.0 + $t + $y = $C2 * $t * $t + $C3 * $t * $t * $t + $C4 * $t * $t * $t * $t + $C5 * $t * $t * $t * $t * $t + if ($index -gt 0) { + $arcLength += [Math]::Sqrt(($x - $previousX) * ($x - $previousX) + ($y - $previousY) * ($y - $previousY)) + } + $heading = [Math]::Atan2( + 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t, + 1.0) + [void]$points.Add((New-GeometryPoint $x $y $arcLength $heading $heading)) + $previousX = $x + $previousY = $y + } + return New-DirectionSegment 0 $forward $points.ToArray() +} + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$processing = $root + 'Processing.' +$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' + +$analyzerType = Get-RequiredType ($processing + 'PathGeometryAnalyzer') +$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') +$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') +$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D') +$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment') +$preparedPathType = Get-RequiredType ($processing + 'PreparedPath') +$analysisType = Get-RequiredType ($processing + 'PathGeometryAnalysis') +$preprocessorType = Get-RequiredType ($processing + 'PathSmoothingPreprocessor') +$resamplerType = Get-RequiredType ($processing + 'ArcLengthResampler') +$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') +$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') +$coarseSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') +$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') +$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap' +$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') +$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') +$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') +$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$smoothingConfigurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') +$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm' +$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest' +$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory' +$validatorType = Get-RequiredType ($root + 'Validation.SmoothedPathValidator') +$rawBaselineBuilderType = Get-RequiredType ($processing + 'RawPathBaselineBuilder') + +Assert-True ($null -ne $preparedPathType) 'PreparedPath must be discoverable for smoothing algorithms.' +Assert-True ($null -ne $preprocessorType) 'PathSmoothingPreprocessor must be discoverable for request preparation.' +Assert-True ($null -ne $resamplerType) 'ArcLengthResampler must be discoverable for deterministic resampling.' +Assert-True ($null -ne $analysisType.GetProperty('MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter')) ` + 'PathGeometryAnalysis must expose peak d-kappa/d-s.' + +$analyzer = [Activator]::CreateInstance($analyzerType) +$analyzeMethod = $analyzerType.GetMethod('TryAnalyze') +Assert-True ($null -ne $analyzeMethod) 'PathGeometryAnalyzer must expose TryAnalyze.' +Assert-Equal 4 $analyzeMethod.GetParameters().Length 'TryAnalyze must accept segments, spacing, analysis, and reason.' +$validator = [Activator]::CreateInstance($validatorType) +$validateMethod = $validatorType.GetMethod('TryValidate') +Assert-True ($null -ne $validateMethod) 'SmoothedPathValidator must expose TryValidate.' +Assert-Equal 9 $validateMethod.GetParameters().Length 'SmoothedPathValidator.TryValidate must retain its public contract.' +$rawBaselineMethod = $rawBaselineBuilderType.GetMethod( + 'TryCreate', + [Reflection.BindingFlags]'Static,NonPublic') +Assert-True ($null -ne $rawBaselineMethod) 'RawPathBaselineBuilder must expose its internal TryCreate path.' + +$forward = [Enum]::Parse($directionType, 'Forward') +$reverse = [Enum]::Parse($directionType, 'Reverse') +$anchor = [Enum]::Parse($sourceType, 'Anchor') +$coarseAnchor = [Enum]::Parse($coarseSourceType, 'Start') + +# The chord-corrected estimator must retain an exact circular curvature limit for both travel directions. +$maximumAllowedCurvature = 5.0 / 6.0 +$circleVehicle = [Activator]::CreateInstance($vehicleType) +$circleVehicle.LengthMeters = 0.20 +$circleVehicle.WidthMeters = 0.20 +$circleVehicle.SafetyMarginMeters = 0.0 +$circleVehicle.MaximumCurvaturePerMeter = $maximumAllowedCurvature +foreach ($direction in @($forward, $reverse)) { + $exactLimitCircle = New-CircularDirectionSegment $direction $maximumAllowedCurvature 20 0.05 + $exactLimitAnalysis = Invoke-Analysis @($exactLimitCircle) + foreach ($point in $exactLimitAnalysis.Path) { + Assert-True ([Math]::Abs($point.VehicleCurvature) -le $maximumAllowedCurvature + 1.0e-9) ` + ('An exact-limit ' + $direction + ' circle must not exceed the curvature limit.') + } + $validation = Invoke-GeometryValidation $exactLimitAnalysis @($exactLimitCircle) $circleVehicle + Assert-True $validation.Accepted ('The validator must accept an analyzed exact-limit ' + $direction + ' circle. Reason=' + $validation.Reason) +} + +# An analyzed over-limit circle must remain detectable by the unchanged validator threshold. +$overLimitCircle = New-CircularDirectionSegment $forward ($maximumAllowedCurvature + 0.01) 20 0.05 +$overLimitAnalysis = Invoke-Analysis @($overLimitCircle) +Assert-True ($overLimitAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter -gt $maximumAllowedCurvature + 1.0e-6) ` + 'An over-limit circle must exceed the vehicle curvature limit by more than the validator tolerance.' +$overLimitValidation = Invoke-GeometryValidation $overLimitAnalysis @($overLimitCircle) $circleVehicle +Assert-False $overLimitValidation.Accepted 'The validator must reject an analyzed over-limit circle.' + +# Trusted raw fallback must apply the exact vehicle-curvature gate before reconstruction. +$trustedLimit = 0.80 +$trustedOverLimit = $trustedLimit + 0.0000005 +$analyzedCurvature = $trustedLimit + 0.01 +$chordLength = 0.05 +$headingStep = 2.0 * [Math]::Asin($analyzedCurvature * $chordLength / 2.0) +$radius = 1.0 / $analyzedCurvature +$trustedPoints = [Array]::CreateInstance($coarsePointType, 21) +for ($index = 0; $index -le 20; $index++) { + $theta = $headingStep * $index + $trustedPoints.SetValue((New-CoarsePathPoint ` + (1.0 + $radius * [Math]::Sin($theta)) ` + (1.0 + $radius * (1.0 - [Math]::Cos($theta))) ` + ($index * $chordLength) ` + $forward ` + $false ` + $(if ($index -eq 0) { 'Start' } else { 'MotionPrimitive' }) ` + $theta ` + $trustedOverLimit), $index) +} +$trustedSegments = [Array]::CreateInstance($coarseSegmentType, 1) +$trustedSegments.SetValue( + [Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 20, $false, $false)), + 0) +$trustedVehicle = [Activator]::CreateInstance($vehicleType) +$trustedVehicle.LengthMeters = 0.20 +$trustedVehicle.WidthMeters = 0.20 +$trustedVehicle.SafetyMarginMeters = 0.0 +$trustedVehicle.MaximumCurvaturePerMeter = $trustedLimit +$trustedConfiguration = [Activator]::CreateInstance($smoothingConfigurationType) +$trustedRequest = [Activator]::CreateInstance($smoothingRequestType, @( + $trustedPoints, + $trustedSegments, + (New-EmptyGeometryMap), + $trustedVehicle, + $trustedConfiguration)) +$trustedPreprocessor = [Activator]::CreateInstance($preprocessorType) +$trustedPrepareMethod = $preprocessorType.GetMethod('TryPrepare') +$trustedPrepareArguments = [object[]]@($trustedRequest, $null, $null) +Assert-True $trustedPrepareMethod.Invoke($trustedPreprocessor, $trustedPrepareArguments) ` + ('Trusted raw fallback regression must prepare successfully. Reason=' + $trustedPrepareArguments[2]) +$trustedBaselineArguments = [object[]]@( + $trustedRequest, + $trustedPrepareArguments[1], + $analyzer, + $trustedConfiguration.OutputSpacingMeters, + $validator, + $trustedConfiguration.MaximumCollisionCheckStepMeters, + $null, + $null) +Assert-False $rawBaselineMethod.Invoke($null, $trustedBaselineArguments) ` + 'Trusted raw fallback must not publish a coarse curvature above the exact vehicle maximum.' +Assert-Equal $overLimitValidation.Reason $trustedBaselineArguments[7] ` + 'Rejecting trusted raw fallback must preserve the original analyzed-path validation failure.' + +# The chord-corrected estimator must not under-estimate these smooth references more than the former polyline estimator. +foreach ($quinticCase in @( + [pscustomobject]@{ Name = 'SBend'; C2 = 0.0; C3 = 0.30; C4 = -0.45; C5 = 0.18; Power = 1.0 }, + [pscustomobject]@{ Name = 'EndpointPeak'; C2 = 0.18; C3 = -0.12; C4 = 0.0; C5 = 0.0; Power = 1.0 }, + [pscustomobject]@{ Name = 'NonUniformFinalInterval'; C2 = -0.12; C3 = 0.36; C4 = -0.30; C5 = 0.08; Power = 1.7 })) { + $quintic = New-QuinticDirectionSegment $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 $quinticCase.Power + $quinticAnalysis = Invoke-Analysis @($quintic) + $analyticMaximum = Get-QuinticAnalyticCurvatureMaximum $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 + $newDeficit = [Math]::Max(0.0, $analyticMaximum - $quinticAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter) + $oldDeficit = [Math]::Max(0.0, $analyticMaximum - (Get-OldPolylineCurvatureMaximum $quinticAnalysis)) + Assert-True ($newDeficit -le $oldDeficit + 1.0e-6) ` + ($quinticCase.Name + ' must not have greater one-sided curvature under-estimation than the old polyline estimator.') +} + +# A half-turn over a single chord has no unambiguous geometric curvature estimate. +$ambiguousTurn = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 ([Math]::PI) ([Math]::PI))) +Assert-AnalysisRejected @($ambiguousTurn) 'A two-point heading turn of π must be rejected as ambiguous.' + +# Forward straight: resampling is exactly 0.05 m, preserves the exact endpoint, and has zero curvature. +$straight = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) +$straightAnalysis = Invoke-Analysis @($straight) +Assert-Equal 21 $straightAnalysis.Path.Count 'A one-metre straight must produce twenty 0.05 m intervals plus the initial point.' +Assert-Near 0.0 $straightAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 0.000000001 ` + 'A straight must have zero peak d-kappa/d-s.' +foreach ($point in $straightAnalysis.Path) { + Assert-Near 0.0 $point.VehicleCurvatureDerivative 0.000000001 ` + 'A straight point must carry zero d-kappa/d-s.' +} +for ($index = 1; $index -lt $straightAnalysis.Path.Count; $index++) { + $left = $straightAnalysis.Path[$index - 1] + $right = $straightAnalysis.Path[$index] + $distance = [Math]::Sqrt(($right.X - $left.X) * ($right.X - $left.X) + ($right.Y - $left.Y) * ($right.Y - $left.Y)) + Assert-Near 0.05 $distance 0.000000001 'Straight resampling intervals must be exactly 0.05 m.' + Assert-Near 0.0 $right.GeometricCurvature 0.000000001 'A forward straight must have zero geometric curvature.' + Assert-Near 0.0 $right.VehicleCurvature 0.000000001 'A forward straight must have zero vehicle curvature.' +} +$straightEnd = $straightAnalysis.Path[$straightAnalysis.Path.Count - 1] +Assert-Near 1.0 $straightEnd.X 0.0 'Resampling must retain the exact final X coordinate.' +Assert-Near 0.0 $straightEnd.Y 0.0 'Resampling must retain the exact final Y coordinate.' + +# Raw coarse anchors carry the vehicle pose, which may differ slightly from the chord tangent of a finite integration step. +$poseAnchoredCurve = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.2 1.0 0.4 0.4)) +$poseAnchoredAnalysis = Invoke-Analysis @($poseAnchoredCurve) +Assert-Near 0.0 $poseAnchoredAnalysis.Path[0].Heading 0.000000000001 'Geometry analysis must retain the first coarse-anchor heading rather than replace it with a chord tangent.' +$poseAnchoredEnd = $poseAnchoredAnalysis.Path[$poseAnchoredAnalysis.Path.Count - 1] +Assert-Near 0.4 $poseAnchoredEnd.Heading 0.000000000001 'Geometry analysis must retain the final coarse-anchor heading rather than replace it with a chord tangent.' + +# A forward R=2 quarter circle has positive +0.5 1/m vehicle curvature. +$forwardArcPoints = New-Object System.Collections.Generic.List[object] +for ($index = 0; $index -le 32; $index++) { + $theta = ([Math]::PI / 2.0) * $index / 32.0 + $x = 2.0 * [Math]::Sin($theta) + $y = 2.0 * (1.0 - [Math]::Cos($theta)) + $arcLength = 2.0 * $theta + [void]$forwardArcPoints.Add((New-GeometryPoint $x $y $arcLength $theta $theta)) +} +$forwardArc = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() +$forwardArcAnalysis = Invoke-Analysis @($forwardArc) +$forwardArcMidpoint = $forwardArcAnalysis.Path[[int]($forwardArcAnalysis.Path.Count / 2)] +Assert-Near 0.5 $forwardArcMidpoint.GeometricCurvature 0.01 'An R=2 quarter circle must have geometric curvature +0.5 1/m.' +Assert-Near 0.5 $forwardArcMidpoint.VehicleCurvature 0.01 'A forward R=2 quarter circle must have vehicle curvature +0.5 1/m.' + +# The same spatial R=2 circle in reverse retains geometric curvature but negates vehicle curvature. +$reverseArc = New-DirectionSegment 0 $reverse $forwardArcPoints.ToArray() +$reverseArcAnalysis = Invoke-Analysis @($reverseArc) +$reverseArcMidpoint = $reverseArcAnalysis.Path[[int]($reverseArcAnalysis.Path.Count / 2)] +Assert-Near 0.5 $reverseArcMidpoint.GeometricCurvature 0.01 'Reverse travel must not change geometric curvature.' +Assert-Near -0.5 $reverseArcMidpoint.VehicleCurvature 0.01 'A reverse R=2 quarter circle must have vehicle curvature -0.5 1/m.' + +# Gear-switch poses are intentionally duplicated: they keep equal arc length and never enter a derivative denominator. +$forwardBeforeSwitch = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true)) $false $true +$reverseAfterSwitch = New-DirectionSegment 1 $reverse @( + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true), + (New-GeometryPoint 0.0 0.0 2.0 0.0 0.0)) $true $false +$switchAnalysis = Invoke-Analysis @($forwardBeforeSwitch, $reverseAfterSwitch) +$firstSegment = $switchAnalysis.Segments[0] +$secondSegment = $switchAnalysis.Segments[1] +$switchLeft = $switchAnalysis.Path[$firstSegment.EndIndex] +$switchRight = $switchAnalysis.Path[$secondSegment.StartIndex] +Assert-Near $switchLeft.X $switchRight.X 0.0 'Gear-switch endpoints must retain duplicate X coordinates.' +Assert-Near $switchLeft.Y $switchRight.Y 0.0 'Gear-switch endpoints must retain duplicate Y coordinates.' +Assert-Near $switchLeft.ArcLength $switchRight.ArcLength 0.0 'Gear-switch endpoints must retain duplicate arc length.' +Assert-Equal 'Forward' $switchLeft.Direction.ToString() 'The first gear-switch pose must retain its forward segment direction.' +Assert-Equal 'Reverse' $switchRight.Direction.ToString() 'The second gear-switch pose must retain its reverse segment direction.' +Assert-True (-not [double]::IsNaN($switchLeft.GeometricCurvature)) 'No derivative may cross the gear-switch duplicate point.' +Assert-True (-not [double]::IsNaN($switchRight.GeometricCurvature)) 'No reverse derivative may cross the gear-switch duplicate point.' +Assert-True (-not [double]::IsNaN($switchLeft.VehicleCurvatureDerivative)) ` + 'The forward side of a gear switch must have a finite one-sided derivative.' +Assert-True (-not [double]::IsNaN($switchRight.VehicleCurvatureDerivative)) ` + 'The reverse side of a gear switch must have a finite one-sided derivative.' + +# Curvature at a curved segment's end and the following straight reverse segment's start must remain independently differentiated. +$forwardArcToSwitch = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() $false $true +$reverseStraightAfterArc = New-DirectionSegment 1 $reverse @( + (New-GeometryPoint 2.0 2.0 0.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0) $true), + (New-GeometryPoint 2.0 1.0 1.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0))) $true $false +$curveSwitchAnalysis = Invoke-Analysis @($forwardArcToSwitch, $reverseStraightAfterArc) +$reverseStraightStart = $curveSwitchAnalysis.Path[$curveSwitchAnalysis.Segments[1].StartIndex] +Assert-Near 0.0 $reverseStraightStart.GeometricCurvature 0.000000001 'A gear-switch must not use the preceding curve to differentiate a reverse straight segment.' +Assert-Near 0.0 $reverseStraightStart.VehicleCurvatureDerivative 0.000000001 ` + 'No curvature derivative may cross from the preceding forward arc into a reverse straight.' + +$physicalStart = New-DirectionSegmentWithStartCurvature 0 $forward 0.20 @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) +$physicalStartAnalysis = Invoke-Analysis @($physicalStart) +Assert-Near 0.20 $physicalStartAnalysis.Path[0].VehicleCurvature 0.000000001 ` + 'The unified analyzer must retain a real start steering-curvature boundary state.' +Assert-True ($physicalStartAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter -gt 0.0) ` + 'A real start-curvature mismatch must remain visible to the quality analyzer.' + +# A boundary that claims a gear switch must be a duplicated pose with opposite direction; discontinuities are rejected. +$invalidSwitch = New-DirectionSegment 1 $reverse @( + (New-GeometryPoint 1.2 0.0 1.0 0.0 0.0 $true), + (New-GeometryPoint 0.2 0.0 2.0 0.0 0.0)) $true $false +Assert-AnalysisRejected @($forwardBeforeSwitch, $invalidSwitch) 'A discontinuous gear-switch boundary must be rejected.' +$trailingGearSwitch = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) $false $true +Assert-AnalysisRejected @($trailingGearSwitch) 'The final direction segment must not advertise a non-existent trailing gear switch.' + +# The public preprocessor receives a raw coarse path and resets every prepared direction segment to local arc length zero. +$coarsePoints = [Array]::CreateInstance($coarsePointType, 4) +$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 0.0 $forward), 0) +$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $forward), 1) +$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $reverse $true), 2) +$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 2.0 $reverse), 3) +$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) +$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $true)), 0) +$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 2, 3, $true, $false)), 1) +$vehicle = [Activator]::CreateInstance($vehicleType) +$vehicle.LengthMeters = 1.0 +$vehicle.WidthMeters = 0.5 +$vehicle.SafetyMarginMeters = 0.0 +$vehicle.MaximumCurvaturePerMeter = 1.0 +$configuration = [Activator]::CreateInstance($configurationType) +$uninitializedMap = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($mapType) +$request = [Activator]::CreateInstance($requestType, @($coarsePoints, $coarseSegments, $uninitializedMap, $vehicle, $configuration)) +$preprocessor = [Activator]::CreateInstance($preprocessorType) +$prepareMethod = $preprocessorType.GetMethod('TryPrepare') +$prepareArguments = [object[]]@($request, $null, $null) +$prepared = $prepareMethod.Invoke($preprocessor, $prepareArguments) +Assert-True $prepared ('Preprocessor must accept a legal forward/reverse raw coarse path. Reason=' + $prepareArguments[2]) +Assert-Near 0.0 $prepareArguments[1].Segments[1].Points[0].ArcLength 0.0 'Every prepared direction segment must begin at local arc length zero.' +Assert-True $prepareArguments[1].Segments[1].Points[0].IsGearSwitchPoint 'The reverse prepared segment must retain its gear-switch point.' + +# Finite coordinates can still overflow distance arithmetic; public resampling must reject them instead of emitting NaN/Infinity. +$overflowSegment = New-DirectionSegment 0 $forward @( + (New-GeometryPoint -1.0e308 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0e308 0.0 1.0 0.0 0.0)) +$resampler = [Activator]::CreateInstance($resamplerType) +$segmentResampleMethod = @($resamplerType.GetMethods() | Where-Object { + $_.Name -eq 'TryResample' -and $_.GetParameters()[0].ParameterType -eq $segmentType +})[0] +$resampleArguments = [object[]]@($overflowSegment, [double]0.05, $null, $null) +$resampled = $segmentResampleMethod.Invoke($resampler, $resampleArguments) +Assert-True (-not $resampled) ('Resampling must reject a distance overflow. Reason=' + $resampleArguments[3]) + +# A prepared path must reject null direction segments rather than silently dropping them during flattening. +$nullSegmentArray = [Array]::CreateInstance($segmentType, 1) +$nullSegmentRejected = $false +try { [void][Activator]::CreateInstance($preparedPathType, @($nullSegmentArray)) } catch { $nullSegmentRejected = $true } +Assert-True $nullSegmentRejected 'PreparedPath must reject a null direction segment.' + +# Unwrapped heading must not jump by 2π when tangents cross the -π/π branch cut. +$crossing = New-DirectionSegment 0 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), + (New-GeometryPoint -1.0 ([Math]::Tan(10.0 * [Math]::PI / 180.0)) 1.015 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)), + (New-GeometryPoint -2.0 0.0 2.03 (-170.0 * [Math]::PI / 180.0) (-170.0 * [Math]::PI / 180.0))) +$crossingAnalysis = Invoke-Analysis @($crossing) +for ($index = 1; $index -lt $crossingAnalysis.Path.Count; $index++) { + $difference = [Math]::Abs($crossingAnalysis.Path[$index].UnwrappedHeading - $crossingAnalysis.Path[$index - 1].UnwrappedHeading) + Assert-True ($difference -lt [Math]::PI) 'Unwrapped headings must remain continuous across the ±π branch cut.' +} + +# The request preprocessor must reset arc length independently for every direction segment, +# while retaining the duplicated pose that represents a legal forward-to-reverse gear switch. +$preprocessor = [Activator]::CreateInstance($preprocessorType) +$prepareMethod = $preprocessorType.GetMethod('TryPrepare') +Assert-True ($null -ne $prepareMethod) 'PathSmoothingPreprocessor must expose TryPrepare.' +$coarsePath = [Array]::CreateInstance($coarsePointType, 5) +$coarsePath.SetValue((New-CoarsePathPoint 0.0 0.0 0.0 $forward $false 'Start'), 0) +$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 1.0 $forward), 1) +$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $forward), 2) +$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $reverse $true), 3) +$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 3.0 $reverse), 4) +$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2) +$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 2, $false, $true)), 0) +$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 3, 4, $true, $false)), 1) +$vehicle = [Activator]::CreateInstance($vehicleType) +$vehicle.LengthMeters = [double]0.80 +$vehicle.WidthMeters = [double]0.60 +$vehicle.SafetyMarginMeters = [double]0.05 +$vehicle.MaximumCurvaturePerMeter = [double]0.80 +$configuration = [Activator]::CreateInstance($smoothingConfigurationType) +$smoothingRequest = [Activator]::CreateInstance($smoothingRequestType, @( + $coarsePath, $coarseSegments, (New-EmptyGeometryMap), $vehicle, $configuration)) +$prepareArguments = [object[]]@($smoothingRequest, $null, $null) +Assert-True $prepareMethod.Invoke($preprocessor, $prepareArguments) ('Preprocessor must accept legal forward/reverse topology. Reason=' + $prepareArguments[2]) +$preparedPath = $prepareArguments[1] +Assert-Equal 2 $preparedPath.Segments.Count 'Preprocessor must preserve both direction segments.' +Assert-Near 0.0 $preparedPath.Segments[1].Points[0].ArcLength 0.0 'The reverse segment must restart local arc length at zero.' +Assert-True $preparedPath.Segments[1].Points[0].IsGearSwitchPoint 'The duplicate reverse gear-switch point must be retained.' + +# Segments may meet only at a paired, coincident forward/reverse gear switch. +$illegalGearJump = New-DirectionSegment 1 $reverse @( + (New-GeometryPoint 1.25 0.0 1.0 0.0 0.0 $true), + (New-GeometryPoint 0.25 0.0 2.0 0.0 0.0)) $true $false +Invoke-RejectedAnalysis @($forwardBeforeSwitch, $illegalGearJump) 'A gear-switch boundary whose poses differ must be rejected.' +$illegalNormalBoundary = New-DirectionSegment 1 $forward @( + (New-GeometryPoint 2.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 3.0 0.0 1.0 0.0 0.0)) $false $false +Invoke-RejectedAnalysis @($straight, $illegalNormalBoundary) 'A non-gear segment boundary must be rejected.' + +# Finite endpoint coordinates can still overflow while computing their separation; reject before interpolation. +$resampler = [Activator]::CreateInstance($resamplerType) +$resamplePointsMethod = $resamplerType.GetMethods() | Where-Object { + $_.Name -eq 'TryResample' -and $_.GetParameters().Length -eq 4 -and + $_.GetParameters()[0].ParameterType -eq [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($pointType) +} | Select-Object -First 1 +Assert-True ($null -ne $resamplePointsMethod) 'ArcLengthResampler must expose point-list TryResample.' +$hugePoints = [Array]::CreateInstance($pointType, 2) +$hugeCoordinate = [double]::MaxValue / 2.0 +$hugePoints.SetValue((New-GeometryPoint (-$hugeCoordinate) 0.0 0.0 0.0 0.0), 0) +$hugePoints.SetValue((New-GeometryPoint $hugeCoordinate 0.0 1.0 0.0 0.0), 1) +$resampleArguments = [object[]]@($hugePoints, [double]0.05, $null, $null) +Assert-False $resamplePointsMethod.Invoke($resampler, $resampleArguments) 'Resampling must reject an infinite geometric distance caused by finite coordinates.' + +# PreparedPath is an all-or-nothing immutable topology snapshot: null direction segments are invalid. +$nullPreparedSegments = [Array]::CreateInstance($segmentType, 1) +Assert-Throws { [Activator]::CreateInstance($preparedPathType, @($nullPreparedSegments)) } 'PreparedPath must reject null direction segments.' + +# Segment indices are deliberately dense and equal to their position in the candidate array. +$sparseSegment = New-DirectionSegment 2 $forward @( + (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0), + (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) +Invoke-RejectedAnalysis @($sparseSegment) 'Prepared direction-segment indices must match their dense array position.' + +Write-Output 'Path smoothing geometry checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_integration.ps1 b/.task8-sweep/tests/verify_path_smoothing_integration.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_integration.ps1 rename to .task8-sweep/tests/verify_path_smoothing_integration.ps1 diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 new file mode 100644 index 0000000..e0c5720 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_local_g2_candidates.ps1 @@ -0,0 +1,252 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { + throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" + } +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +function Get-InternalProperty($Instance, [string]$Name) { + return $Instance.GetType().GetProperty( + $Name, + [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($Instance) +} + +function Get-InternalMethod($Type, [string]$Name) { + return @($Type.GetMethods([Reflection.BindingFlags]'Public,NonPublic,Instance,Static') | + Where-Object Name -eq $Name)[0] +} + +function New-InternalInstance($Type) { + return [Activator]::CreateInstance( + $Type, + [Reflection.BindingFlags]'Instance,NonPublic,Public', + $null, + @(), + $null) +} + +$builderType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateBuilder' +$hooksType = $builderType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') +Assert-True ($null -ne $hooksType) 'LocalG2CandidateBuilder must expose narrowly scoped deterministic TestHooks.' +$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') +Assert-True ($null -ne $executeMethod) 'TestHooks must execute deterministic candidate scenarios.' + +function Invoke-Scenario([string]$Scenario) { + return $executeMethod.Invoke($null, @($Scenario)) +} + +$isolated = Invoke-Scenario 'Isolated' +Assert-True ($isolated.CandidateCount -gt 0 -and $isolated.CandidateCount -le 12) ` + 'An isolated event must produce a bounded non-empty candidate set.' +Assert-Near 0.0 $isolated.StartPositionError 1e-9 'Window start position must match.' +Assert-Near 0.0 $isolated.EndPositionError 1e-9 'Window end position must match.' +Assert-Near 0.0 $isolated.StartCurvatureError 1e-8 'Window start curvature must match.' +Assert-Near 0.0 $isolated.EndCurvatureError 1e-8 'Window end curvature must match.' +Assert-True $isolated.ContainsLocalG2Source 'Generated samples must identify their source.' + +$cluster = Invoke-Scenario 'Cluster' +Assert-Equal 1 $cluster.OutputRegionCount 'Overlapping events must be built as one region.' +Assert-True $cluster.InternalConnectionsAreG2 'Internal anchors must share tangent and curvature.' + +$reverse = Invoke-Scenario 'Reverse' +Assert-Equal 'Reverse' $reverse.Direction 'Reverse candidates must preserve segment direction.' +Assert-True $reverse.VehicleAndGeometricCurvatureSignsAreOpposite ` + 'Reverse candidates must convert vehicle curvature to geometric curvature exactly once.' + +$spliced = Invoke-Scenario 'Spliced' +Assert-True $spliced.NoDuplicateNonGearPoints 'Splicing must remove duplicate boundary samples.' +Assert-True $spliced.EndpointsUnchanged 'Splicing must preserve the complete segment endpoints.' + +$startBoundary = Invoke-Scenario 'StartBoundary' +Assert-True ($startBoundary.CandidateCount -gt 0) 'A start-boundary event must build a one-sided candidate.' +Assert-Near 0.0 $startBoundary.StartCurvatureError 1e-8 ` + 'A start-boundary candidate must use the preserved physical start vehicle curvature.' + +$interiorDerivative = Invoke-Scenario 'InteriorStationaryCurve' +Assert-True $interiorDerivative.Rejected ` + 'A curve with a stationary interior derivative must be rejected even when its endpoint chord is short.' + +$constantVelocity = Invoke-Scenario 'ConstantVelocityCurve' +Assert-True $constantVelocity.Accepted ` + 'A nonstationary constant-velocity curve with collinear derivative controls must be accepted.' + +$exactSplice = Invoke-Scenario 'ExactSpliceEndpoints' +Assert-True $exactSplice.EndpointsAreExact ` + 'Splicing must write interpolated exact endpoints instead of accepting approximate candidate endpoints.' + +$gearBoundary = Invoke-Scenario 'GearBoundary' +Assert-True $gearBoundary.GearBoundaryMarkerPreserved ` + 'A window touching a gear-switch segment boundary must preserve its point-level gear marker.' + +$twoRegions = Invoke-Scenario 'TwoRegionWorkOrder' +Assert-True $twoRegions.WorkOrderDescending ` + 'Same-segment regions must be processed from larger original local arc to smaller local arc.' +Assert-True $twoRegions.FrontArcPreservedAfterBackReplacement ` + 'Replacing the back region must preserve the front region original arc coordinates.' +Assert-True $twoRegions.BothReplacementsRetained ` + 'Back-then-front replacement must retain the exact LocalG2Transition interiors at (0.75, 0.20) and (2.25, 0.20).' +Assert-True $twoRegions.ForwardOrderRejected ` + 'The regression fixture must prove that front-first invalidates the original back absolute arc.' +Assert-True $twoRegions.DeterministicWorkOrder ` + 'Work ordering must repeat exactly without mutating report order.' +Assert-True $twoRegions.DeterministicReplacementGeometry ` + 'Repeated back-then-front replacement must preserve count and every point X, Y, source, and direction.' +Assert-True $twoRegions.InvalidWorkOrderRejected ` + 'Work ordering must reject null regions and cross-segment event contents.' + +$ordering = Invoke-Scenario 'MultiSegmentWorkOrder' +Assert-True $ordering.SegmentOrderAscending ` + 'Region work ordering must process segment indices in ascending order.' +Assert-True $ordering.EqualArcUsesReportOrder ` + 'Equal first-local-arc regions must retain their original report-index order.' + +$evaluatorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2CandidateEvaluator' +$evaluatorHooksType = $evaluatorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') +Assert-True ($null -ne $evaluatorHooksType) 'LocalG2CandidateEvaluator must expose narrowly scoped deterministic TestHooks.' +$evaluateMethod = $evaluatorHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') +Assert-True ($null -ne $evaluateMethod) 'TestHooks must execute deterministic candidate quality-gate scenarios.' + +function Invoke-EvaluationScenario([string]$Scenario) { + return $evaluateMethod.Invoke($null, @($Scenario)) +} + +$tooFar = Invoke-EvaluationScenario 'TooFar' +Assert-Equal 'DeviationExceeded' $tooFar.FailureReason ` + 'A collision-free candidate more than 0.10 m from the raw window must be rejected.' + +$overshoot = Invoke-EvaluationScenario 'Overshoot' +Assert-Equal 'CurvatureOvershoot' $overshoot.FailureReason ` + 'A candidate outside the raw regional curvature range must be rejected.' + +$noOp = Invoke-EvaluationScenario 'NoOp' +Assert-Equal 'InsufficientImprovement' $noOp.FailureReason ` + 'A safe no-op must not be accepted.' + +$oscillating = Invoke-EvaluationScenario 'Oscillating' +Assert-Equal 'VariationCostRegression' $oscillating.FailureReason ` + 'Repeated curvature oscillation must fail the variation-cost gate.' + +$lowClearance = Invoke-EvaluationScenario 'LowClearance' +Assert-Equal 'InsufficientClearance' $lowClearance.FailureReason ` + 'A path with less than 0.02 m checked clearance must be rejected.' + +$improved = Invoke-EvaluationScenario 'Improved' +Assert-Equal 'Accepted' $improved.Status ` + 'A safe candidate with at least 20 percent peak improvement must be accepted.' + +$smallestDeviation = Invoke-EvaluationScenario 'SmallestDeviation' +$best = Invoke-EvaluationScenario 'Best' +Assert-Equal $smallestDeviation.CandidateIndex $best.CandidateIndex ` + 'Among sufficient candidates, minimum deviation must win before extra smoothness.' + +# Exercise the real builder → evaluator seam that a hand-built TestHook candidate does not cover. +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$factoryType = Get-RequiredType ($root + 'Test.SmoothingScenarioFactory') +$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$methodType = Get-RequiredType ($root + 'SmoothingMethod') +$preprocessorType = Get-RequiredType ($root + 'Processing.PathSmoothingPreprocessor') +$optionsType = Get-RequiredType ($root + 'LocalG2.LocalG2OptionsSnapshot') +$detectorType = Get-RequiredType ($root + 'LocalG2.CurvatureTransitionDetector') +$plannerType = Get-RequiredType ($root + 'LocalG2.LocalG2WindowPlanner') +$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' +$fixtures = (Get-InternalMethod $factoryType 'CreateFixtureRequests').Invoke($null, @((Resolve-Path $fixturePath).Path)) +$baseRequest = $fixtures[1].SmoothingRequest # single-turn +$configuration = [Activator]::CreateInstance($configurationType) +$configuration.Method = [Enum]::Parse($methodType, 'LocalG2Quintic') +$singleTurnRequest = [Activator]::CreateInstance($requestType, @( + $baseRequest.CoarsePath, $baseRequest.Segments, $baseRequest.Map, $baseRequest.Vehicle, $configuration)) +$preprocessor = [Activator]::CreateInstance($preprocessorType) +$prepareArgs = [object[]]@($singleTurnRequest, $null, $null) +Assert-True ((Get-InternalMethod $preprocessorType 'TryPrepare').Invoke($preprocessor, $prepareArgs)) ` + 'SingleTurn must prepare before evaluating Local G2 candidates.' +$preparedPath = $prepareArgs[1] +$options = [Activator]::CreateInstance($optionsType, [Reflection.BindingFlags]'Instance,NonPublic,Public', $null, @($configuration), $null) +$detector = New-InternalInstance $detectorType +$detectArgs = [object[]]@($singleTurnRequest, [double]$singleTurnRequest.Vehicle.MaximumCurvaturePerMeter, $options, $null, $null) +Assert-True ((Get-InternalMethod $detectorType 'TryDetect').Invoke($detector, $detectArgs)) ` + 'SingleTurn must detect its Local G2 transition.' +$planner = New-InternalInstance $plannerType +$planArgs = [object[]]@($preparedPath, $detectArgs[3], $options, $null, $null) +Assert-True ((Get-InternalMethod $plannerType 'TryPlan').Invoke($planner, $planArgs)) ` + 'SingleTurn must plan a Local G2 region.' +$region = $null +foreach ($plannedRegion in $planArgs[3]) { $region = $plannedRegion; break } +$preparedSegment = (Get-InternalProperty $preparedPath 'Segments')[(Get-InternalProperty $region 'SegmentIndex')] +$realBuilder = New-InternalInstance $builderType +$realCandidates = (Get-InternalMethod $builderType 'Build').Invoke($realBuilder, @( + $preparedSegment, $region, [double]$configuration.OutputSpacingMeters, $options, [Threading.CancellationToken]::None)) +Assert-True ($realCandidates.Count -gt 0) 'SingleTurn must build real Local G2 candidates.' +$realEvaluator = New-InternalInstance $evaluatorType +$duplicateFailures = @() +$acceptedCandidates = @() +$extractWindow = $evaluatorType.GetMethod('TryExtractWindow', [Reflection.BindingFlags]'NonPublic,Static') +$analyzeWindow = $evaluatorType.GetMethod('TryAnalyzeWindow', [Reflection.BindingFlags]'NonPublic,Instance') +foreach ($realCandidate in $realCandidates) { + $evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @( + $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, [Threading.CancellationToken]::None)) + $failureReason = Get-InternalProperty $evaluation 'FailureReason' + if ($failureReason -eq 2) { + $candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' + $duplicateFailures += $candidateIndex + } + if (Get-InternalProperty $evaluation 'Accepted') { + $acceptedCandidates += (Get-InternalProperty $realCandidate 'CandidateIndex') + } + $windowArgs = [object[]]@($preparedSegment, + (Get-InternalProperty $realCandidate 'StartArcLengthMeters'), + (Get-InternalProperty $realCandidate 'EndArcLengthMeters'), $null, $null) + $null = $extractWindow.Invoke($null, $windowArgs) + $rawWindowArgs = [object[]]::new(5) + $rawWindowArgs[0] = $preparedSegment + $rawWindowArgs[1] = $windowArgs[3] + $rawWindowArgs[2] = [double]$configuration.OutputSpacingMeters + $candidateWindowArgs = [object[]]::new(5) + $candidateWindowArgs[0] = $preparedSegment + $candidateWindowArgs[1] = $realCandidate.GetType().GetProperty( + 'RegionPoints', [Reflection.BindingFlags]'Public,NonPublic,Instance').GetValue($realCandidate) + $candidateWindowArgs[2] = [double]$configuration.OutputSpacingMeters + $null = $analyzeWindow.Invoke($realEvaluator, $rawWindowArgs) + $null = $analyzeWindow.Invoke($realEvaluator, $candidateWindowArgs) + $rawPath = Get-InternalProperty $rawWindowArgs[3] 'Path' + $rawMinimum = [double]::PositiveInfinity + $rawMaximum = [double]::NegativeInfinity + foreach ($point in $rawPath) { + $rawMinimum = [Math]::Min($rawMinimum, $point.VehicleCurvature) + $rawMaximum = [Math]::Max($rawMaximum, $point.VehicleCurvature) + } + $rawPeak = Get-InternalProperty $rawWindowArgs[3] 'MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter' + $candidatePeak = Get-InternalProperty $candidateWindowArgs[3] 'MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter' + $candidateMaximum = Get-InternalProperty $candidateWindowArgs[3] 'MaximumAbsoluteVehicleCurvaturePerMeter' + $candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' + $fraction = @(0.00, 0.10, 0.20)[[Math]::Floor($candidateIndex / 4)] + $scale = @(1.00, 1.20, 1.40, 1.60)[$candidateIndex % 4] + Write-Output ('SWEEP candidate={0}; alpha={1:F2}; internalScale={2:F2}; window={3:R}..{4:R}; result={5}; rawK=[{6:R},{7:R}]; maxK={8:R}; rawPeak={9:R}; peak={10:R}' -f + $candidateIndex, $fraction, $scale, + (Get-InternalProperty $realCandidate 'StartArcLengthMeters'), (Get-InternalProperty $realCandidate 'EndArcLengthMeters'), + $failureReason, $rawMinimum, $rawMaximum, $candidateMaximum, $rawPeak, $candidatePeak) +} +Assert-Equal 0 $duplicateFailures.Count ` + 'SingleTurn builder candidates must not fail evaluator window analysis due to duplicate or degenerate points.' +Assert-True ($acceptedCandidates.Count -gt 0) ` + 'SingleTurn must produce at least one builder candidate accepted by the unchanged evaluator gates.' + +Write-Output 'Path smoothing Local G2 candidate checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 new file mode 100644 index 0000000..2187dc4 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_local_g2_curve.ps1 @@ -0,0 +1,118 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { + throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" + } +} + +function Assert-Rejected($Result, [string]$Message) { + if ($Result.Accepted) { throw $Message } + Assert-True (-not [string]::IsNullOrWhiteSpace($Result.Reason)) 'Rejected curves must provide a stable reason.' +} + +function Assert-Throws([scriptblock]$Action, [string]$Message) { + try { + & $Action + } + catch [ArgumentOutOfRangeException] { + return + } + catch [System.Reflection.TargetInvocationException] { + Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) $Message + return + } + throw $Message +} + +$curveType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.QuinticHermiteCurve2D', $true) +$tryCreate = $curveType.GetMethod('TryCreate', [Reflection.BindingFlags]'Static,NonPublic') +$evaluate = $curveType.GetMethod('Evaluate', [Reflection.BindingFlags]'Instance,NonPublic') +Assert-True ($null -ne $tryCreate) 'QuinticHermiteCurve2D must expose its internal TryCreate factory.' +Assert-True ($null -ne $evaluate) 'QuinticHermiteCurve2D must expose its internal Evaluate method.' + +function Invoke-Curve( + [double]$X0, [double]$Y0, [double]$Dx0, [double]$Dy0, [double]$Ddx0, [double]$Ddy0, + [double]$X1, [double]$Y1, [double]$Dx1, [double]$Dy1, [double]$Ddx1, [double]$Ddy1) { + $arguments = [object[]]@($X0, $Y0, $Dx0, $Dy0, $Ddx0, $Ddy0, $X1, $Y1, $Dx1, $Dy1, $Ddx1, $Ddy1, $null, $null) + $accepted = $tryCreate.Invoke($null, $arguments) + Assert-True $accepted "Quintic Hermite curve creation must succeed. Reason=$($arguments[13])" + return $arguments[12] +} + +function Invoke-InvalidCurve([string]$CaseName) { + $arguments = switch ($CaseName) { + 'ZeroFirstDerivative' { [object[]]@(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } + 'NonFiniteInput' { [object[]]@(0.0, 0.0, [double]::NaN, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 0.0, 0.0, $null, $null) } + default { throw "Unknown invalid curve case: $CaseName" } + } + return [PSCustomObject]@{ + Accepted = $tryCreate.Invoke($null, $arguments) + Reason = [string]$arguments[13] + } +} + +function Invoke-Evaluate($Curve, [double]$U) { + $arguments = [object[]]@($U, $null, $null, $null, $null, $null, $null) + $evaluate.Invoke($Curve, $arguments) + return [PSCustomObject]@{ + X = [double]$arguments[1]; Y = [double]$arguments[2] + Dx = [double]$arguments[3]; Dy = [double]$arguments[4] + Ddx = [double]$arguments[5]; Ddy = [double]$arguments[6] + } +} + +function Get-Curvature($Sample) { + return ($Sample.Dx * $Sample.Ddy - $Sample.Dy * $Sample.Ddx) / + [Math]::Pow($Sample.Dx * $Sample.Dx + $Sample.Dy * $Sample.Dy, 1.5) +} + +function Assert-Boundary($Expected, $Actual, [string]$Name) { + Assert-Near $Expected.X $Actual.X 1e-10 "$Name X must match." + Assert-Near $Expected.Y $Actual.Y 1e-10 "$Name Y must match." + Assert-Near $Expected.Dx $Actual.Dx 1e-10 "$Name dX must match." + Assert-Near $Expected.Dy $Actual.Dy 1e-10 "$Name dY must match." + Assert-Near $Expected.Ddx $Actual.Ddx 1e-10 "$Name ddX must match." + Assert-Near $Expected.Ddy $Actual.Ddy 1e-10 "$Name ddY must match." +} + +$straight = Invoke-Curve 0 0 1 0 0 0 2 0 1 0 0 0 +$straightStart = Invoke-Evaluate $straight 0.0 +$straightEnd = Invoke-Evaluate $straight 1.0 +$straightMid = Invoke-Evaluate $straight 0.5 +Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightStart 'Straight start' +Assert-Boundary ([PSCustomObject]@{ X = 2.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.0 }) $straightEnd 'Straight end' +Assert-Near 0.0 $straightMid.Y 1e-12 'A straight Hermite curve must remain on the axis.' + +$curved = Invoke-Curve 0 0 1 0 0 0.4 1 1 0 1 -0.4 0 +$curvedStart = Invoke-Evaluate $curved 0.0 +$curvedEnd = Invoke-Evaluate $curved 1.0 +Assert-Boundary ([PSCustomObject]@{ X = 0.0; Y = 0.0; Dx = 1.0; Dy = 0.0; Ddx = 0.0; Ddy = 0.4 }) $curvedStart 'Curved start' +Assert-Boundary ([PSCustomObject]@{ X = 1.0; Y = 1.0; Dx = 0.0; Dy = 1.0; Ddx = -0.4; Ddy = 0.0 }) $curvedEnd 'Curved end' +Assert-Near 0.4 (Get-Curvature $curvedStart) 1e-10 'Start curvature must match boundary derivatives.' +Assert-Near 0.4 (Get-Curvature $curvedEnd) 1e-10 'End curvature must match boundary derivatives.' + +$allFinite = $true +foreach ($u in @(0.0, 0.25, 0.5, 0.75, 1.0)) { + $sample = Invoke-Evaluate $curved $u + foreach ($value in @($sample.X, $sample.Y, $sample.Dx, $sample.Dy, $sample.Ddx, $sample.Ddy)) { + $allFinite = $allFinite -and (-not [double]::IsNaN($value)) -and (-not [double]::IsInfinity($value)) + } +} +Assert-True $allFinite 'All sampled values must be finite.' + +Assert-Rejected (Invoke-InvalidCurve 'ZeroFirstDerivative') ` + 'A curve endpoint with zero first derivative must be rejected.' +Assert-Rejected (Invoke-InvalidCurve 'NonFiniteInput') ` + 'A curve with a non-finite boundary value must be rejected.' +Assert-Throws { Invoke-Evaluate $curved -0.001 } 'Evaluate must reject parameters below zero.' +Assert-Throws { Invoke-Evaluate $curved 1.001 } 'Evaluate must reject parameters above one.' + +Write-Output 'Local G2 quintic curve checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 new file mode 100644 index 0000000..7b54982 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_local_g2_detection.ps1 @@ -0,0 +1,129 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { + throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" + } +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +$detectorType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.CurvatureTransitionDetector' +$hooksType = $detectorType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') +Assert-True ($null -ne $hooksType) 'CurvatureTransitionDetector must expose its narrowly scoped nested TestHooks helper.' +$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') +Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.' + +function Invoke-Scenario([string]$Scenario) { + return $executeMethod.Invoke($null, @($Scenario)) +} + +# Same direction: 0 -> 0.4167 is detected once. +$singleTransition = Invoke-Scenario 'SingleTransition' +Assert-Equal 1 $singleTransition.TransitionCount 'One primitive curvature jump must be detected.' +Assert-Near 0.4167 $singleTransition.MaximumJump 0.0001 'The jump magnitude must be retained.' + +# Same pose at a forward/reverse boundary: no event crosses the stop. +$gearSwitch = Invoke-Scenario 'GearSwitch' +Assert-Equal 0 $gearSwitch.TransitionCount 'A stopped gear switch must not be a smoothing event.' + +# A 0.01 1/m numerical change is below max(0.001, 5% of 0.8333). +$noise = Invoke-Scenario 'Noise' +Assert-Equal 0 $noise.TransitionCount 'Sub-threshold curvature noise must be ignored.' + +# Two 0.50 m windows whose ranges overlap are merged. +$overlap = Invoke-Scenario 'Overlap' +Assert-Equal 1 $overlap.RegionCount 'Overlapping windows must form one joint region.' +Assert-Equal 2 $overlap.TransitionCountInFirstRegion 'The merged region must retain both events.' + +# Near a segment start, the window becomes asymmetric without crossing the hard boundary. +$nearStart = Invoke-Scenario 'NearStart' +Assert-Near 0.0 $nearStart.StartArcLength 0.000000001 'A start window must be clamped to the segment.' +Assert-True ($nearStart.RightWindowLength -gt $nearStart.LeftWindowLength) ` + 'Unavailable left length must be shifted to the right.' + +# Finite endpoint curvatures may still overflow during subtraction; detection must fail rather than publish infinity. +$coarsePathPointType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPoint' +$pathSegmentType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.PathSegment' +$directionType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.TravelDirection' +$coarseSourceType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.CoarsePath.CoarsePathPointSource' +$requestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingRequest' +$configurationType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.PathSmoothingConfiguration' +$optionsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2OptionsSnapshot' +$forward = [Enum]::Parse($directionType, 'Forward') +$motionPrimitive = [Enum]::Parse($coarseSourceType, 'MotionPrimitive') +$overflowPoints = [Array]::CreateInstance($coarsePathPointType, 2) +$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( + [double]0.0, [double]0.0, [double]0.0, [double]0.0, [double]0.0, $forward, + -[double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 0) +$overflowPoints.SetValue([Activator]::CreateInstance($coarsePathPointType, @( + [double]0.1, [double]0.0, [double]0.0, [double]0.0, [double]0.1, $forward, + [double]::MaxValue, [double]1.0, $false, $motionPrimitive)), 1) +$overflowSegments = [Array]::CreateInstance($pathSegmentType, 1) +$overflowSegments.SetValue([Activator]::CreateInstance($pathSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) +$configuration = [Activator]::CreateInstance($configurationType) +$optionsConstructor = $optionsType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, @($configurationType), $null) +Assert-True ($null -ne $optionsConstructor) 'Local G2 options must be constructible for detector reflection tests.' +$options = $optionsConstructor.Invoke(@($configuration)) +$overflowRequest = [Activator]::CreateInstance($requestType, @($overflowPoints, $overflowSegments, $null, $null, $configuration)) +$detector = [Activator]::CreateInstance($detectorType, $true) +$tryDetect = $detectorType.GetMethod('TryDetect', [Reflection.BindingFlags]'Instance,NonPublic') +Assert-True ($null -ne $tryDetect) 'CurvatureTransitionDetector must retain its internal TryDetect contract.' +$overflowArguments = [object[]]@($overflowRequest, [double]0.8333, $options, $null, $null) +$overflowAccepted = $tryDetect.Invoke($detector, $overflowArguments) +Assert-False $overflowAccepted 'Curvature subtraction overflow must reject detection.' +Assert-True (-not [string]::IsNullOrWhiteSpace([string]$overflowArguments[4])) ` + 'Rejected overflow detection must provide a stable reason.' +Assert-Equal 0 $overflowArguments[3].Count 'Rejected overflow detection must not publish a curvature event.' + +$plannerType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2.LocalG2WindowPlanner' +$plannerHooksType = $plannerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') +Assert-True ($null -ne $plannerHooksType) 'LocalG2WindowPlanner must expose narrow deterministic TestHooks.' +$planScenario = $plannerHooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') + +$separated = $planScenario.Invoke($null, @('SeparatedByOneMeter')) +Assert-Equal 2 $separated.RegionCount 'Events 1.0 m apart cannot share a 0.80 m total window.' +Assert-Equal '1,1' $separated.TransitionCounts 'Separated events must remain one event per region.' + +$mergeable = $planScenario.Invoke($null, @('Mergeable')) +Assert-Equal 1 $mergeable.RegionCount 'Events covered by one legal total window must merge.' +Assert-Equal '2' $mergeable.TransitionCounts 'The merged region must retain both events.' + +$partition = $planScenario.Invoke($null, @('ThreeEventPartition')) +Assert-Equal 2 $partition.RegionCount 'Three events must split at the first infeasible joint window.' +Assert-Equal '2,1' $partition.TransitionCounts 'Only a feasible consecutive subgroup may merge.' + +$boundary = $planScenario.Invoke($null, @('NearBoundary')) +Assert-True ($boundary.FirstRightLength -gt $boundary.FirstLeftLength) ` + 'A boundary-clamped total window must transfer missing length to the available side.' +Assert-True ($boundary.MaximumWindowLength -le 0.80 + 1e-9) ` + 'No candidate window may exceed 0.80 m total length.' + +foreach ($snapshot in @($separated, $mergeable, $partition, $boundary)) { + Assert-True $snapshot.ExactEnvelope 'Region envelope must equal the extrema of actual legal variants.' + Assert-True ($snapshot.MaximumWindowLength -le 0.80 + 1e-9) ` + 'MaximumWindowLengthMeters is a total, not a per-side length.' +} + +$repeat = $planScenario.Invoke($null, @('ThreeEventPartition')) +Assert-Equal $partition.Signature $repeat.Signature ` + 'Repeated planning must preserve grouping, candidate numbering and variant order.' + +Write-Output 'Path smoothing Local G2 detection checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 b/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 new file mode 100644 index 0000000..0a9aab4 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_local_g2_integration.ps1 @@ -0,0 +1,136 @@ +param( + [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') +) + +$ErrorActionPreference = 'Stop' +$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { + throw "$Message Expected=$Expected Actual=$Actual" + } +} +function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$facade = $root + 'Facade.' +$test = $root + 'Test.' +$serviceType = Get-RequiredType ($facade + 'PathSmoothingService') +$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$methodType = Get-RequiredType ($root + 'SmoothingMethod') +$factoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') +$localG2 = [Enum]::Parse($methodType, 'LocalG2Quintic') +$service = [Activator]::CreateInstance($serviceType) +$smooth = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken])) +$createFixtures = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) +Assert-True ($null -ne $smooth) 'Local G2 must use the public service entrypoint.' + +function New-LocalG2Request($BaseRequest, [scriptblock]$Configure = $null) { + $configuration = [Activator]::CreateInstance($configurationType) + $configuration.Method = $localG2 + $configuration.AllowFallbackToCoarsePath = $true + if ($null -ne $Configure) { & $Configure $configuration } + return [Activator]::CreateInstance($requestType, @( + $BaseRequest.CoarsePath, $BaseRequest.Segments, $BaseRequest.Map, + $BaseRequest.Vehicle, $configuration)) +} + +function Invoke-LocalG2($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) { + return $smooth.Invoke($service, @($Request, $CancellationToken)) +} + +function Assert-PublishedPath($Result, [string]$Name) { + Assert-True ($Result.Path.Count -gt 0) "$Name must publish a complete path." + Assert-Equal 0 $Result.Segments[0].StartIndex "$Name segments must start at zero." + Assert-Equal ($Result.Path.Count - 1) $Result.Segments[-1].EndIndex "$Name segments must cover the path." + Assert-True $Result.Diagnostics.Metrics.IsFeasible "$Name diagnostics must be feasible." + Assert-True ($Result.Diagnostics.Metrics.MinimumBodyClearanceMeters -ge 0.02) "$Name must retain the configured clearance reserve." +} + +function Assert-Equivalent($First, $Second, [string]$Name) { + Assert-Equal $First.Status $Second.Status "$Name must retain deterministic status." + Assert-Equal $First.Path.Count $Second.Path.Count "$Name must retain deterministic point count." + Assert-Equal $First.RegionReports.Count $Second.RegionReports.Count "$Name must retain deterministic report count." + for ($index = 0; $index -lt $First.Path.Count; $index++) { + Assert-Near $First.Path[$index].X $Second.Path[$index].X "$Name point $index must retain X." + Assert-Near $First.Path[$index].Y $Second.Path[$index].Y "$Name point $index must retain Y." + } + for ($index = 0; $index -lt $First.RegionReports.Count; $index++) { + Assert-Equal $First.RegionReports[$index].SelectedCandidateIndex $Second.RegionReports[$index].SelectedCandidateIndex "$Name report $index must retain candidate selection." + Assert-Equal $First.RegionReports[$index].Status $Second.RegionReports[$index].Status "$Name report $index must retain status." + Assert-Near $First.RegionReports[$index].StartArcLengthMeters $Second.RegionReports[$index].StartArcLengthMeters "$Name report $index must retain order." + } +} + +$fixtures = $createFixtures.Invoke($null, @((Resolve-Path $FixturePath).Path)) +$expected = @{ + 'straight' = @('NotNeeded') + 'single-turn' = @('Complete') + 'large-heading-change' = @('Complete') + 's-bend' = @('Complete', 'PartialImprovement') + 'rectangle-detour' = @('Complete', 'PartialImprovement') + 'multi-obstacle-detour' = @('Complete', 'PartialImprovement') + 'forward-reverse-switch' = @('Complete', 'PartialImprovement', 'NotNeeded') +} + +$results = @{} +foreach ($fixture in $fixtures) { + $request = New-LocalG2Request $fixture.SmoothingRequest + $first = Invoke-LocalG2 $request + $second = Invoke-LocalG2 $request + $scenario = $fixture.SmoothingRequest.CoarsePath[0].Source.ToString() # fixture index is resolved below by stable input order + $results[$results.Count] = $first + Write-Output ("Fixture $($results.Count - 1): status=$($first.Status); reports=$($first.RegionReports.Count); reason=$($first.Diagnostics.Reason)") + foreach ($report in $first.RegionReports) { + Write-Output (" region=$($report.Status)/$($report.FailureReason); candidates=$($report.CandidateCount); selected=$($report.SelectedCandidateIndex)") + } + Assert-Equivalent $first $second "Fixture $($results.Count - 1)" +} + +$fixtureIds = @('straight', 'single-turn', 's-bend', 'large-heading-change', 'rectangle-detour', 'multi-obstacle-detour', 'narrow-corridor', 'forward-reverse-switch') +for ($index = 0; $index -lt $fixtureIds.Count; $index++) { + $id = $fixtureIds[$index] + if (-not $expected.ContainsKey($id)) { continue } + $result = $results[$index] + Assert-True ($expected[$id] -contains $result.Status.ToString()) "$id must publish its required Local G2 status." + Assert-PublishedPath $result $id + if ($result.Status.ToString() -in @('Complete', 'PartialImprovement')) { + Assert-True (@($result.RegionReports | Where-Object { $_.Status.ToString() -eq 'Improved' }).Count -gt 0) "$id must report an accepted region." + } + if ($result.Status.ToString() -eq 'PartialImprovement') { + Assert-True (@($result.RegionReports | Where-Object { $_.Status.ToString() -eq 'RetainedOriginal' }).Count -gt 0) "$id must report retained regions." + } +} + +# The multi-obstacle fixture is a same-direction two-window integration case. Its two replacements +# change length, so processing it front-to-back would invalidate the later original arc window. +$twoRegion = $results[5] +Assert-True ($twoRegion.RegionReports.Count -ge 2) 'Two-region integration fixture must publish both detector regions.' +Assert-Equal 'Improved' $twoRegion.RegionReports[0].Status.ToString() 'First detector-order two-region report must improve.' +Assert-Equal 'Improved' $twoRegion.RegionReports[1].Status.ToString() 'Second detector-order two-region report must improve.' +Assert-True ($twoRegion.RegionReports[0].StartArcLengthMeters -lt $twoRegion.RegionReports[1].StartArcLengthMeters) 'Reports must retain detector ascending arc order.' +Assert-True (@($twoRegion.Path | Where-Object { $_.Source.ToString() -eq 'LocalG2Transition' }).Count -gt 2) 'Both Local G2 replacements must remain in the final path.' + +$strictRequest = New-LocalG2Request $fixtures[1].SmoothingRequest { + param($configuration) + $configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = 0.99 +} +$unchanged = Invoke-LocalG2 $strictRequest +Assert-Equal 'Unchanged' $unchanged.Status.ToString() 'Detected events with no accepted region must publish a verified raw path as Unchanged.' +Assert-PublishedPath $unchanged 'Unchanged Local G2' + +$cancelSource = [Threading.CancellationTokenSource]::new() +$cancelSource.Cancel() +$cancelled = Invoke-LocalG2 (New-LocalG2Request $fixtures[1].SmoothingRequest) $cancelSource.Token +Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancellation must propagate through Local G2.' +Assert-Equal 0 $cancelled.Path.Count 'A cancelled run must not publish a partial path.' + +Write-Output 'Local G2 service integration checks passed.' diff --git a/.task8-sweep/tests/verify_path_smoothing_png.ps1 b/.task8-sweep/tests/verify_path_smoothing_png.ps1 new file mode 100644 index 0000000..b5e526c --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_png.ps1 @@ -0,0 +1,18 @@ +param( + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') +) + +$ErrorActionPreference = 'Stop' + +$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' +if (-not (Test-Path -LiteralPath $hostProject)) { + throw "Path smoothing PNG verification host was not found: $hostProject" +} + +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path +& dotnet run --project $hostProject --no-restore -- $resolvedFixturePath +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +Write-Output 'Path smoothing PNG checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_quintic.ps1 b/.task8-sweep/tests/verify_path_smoothing_quintic.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_quintic.ps1 rename to .task8-sweep/tests/verify_path_smoothing_quintic.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_runner.ps1 b/.task8-sweep/tests/verify_path_smoothing_runner.ps1 similarity index 100% rename from ClumsyPilot/tests/verify_path_smoothing_runner.ps1 rename to .task8-sweep/tests/verify_path_smoothing_runner.ps1 diff --git a/ClumsyPilot/tests/verify_path_smoothing_service.ps1 b/.task8-sweep/tests/verify_path_smoothing_service.ps1 similarity index 96% rename from ClumsyPilot/tests/verify_path_smoothing_service.ps1 rename to .task8-sweep/tests/verify_path_smoothing_service.ps1 index 7bd0b96..704e9a7 100644 --- a/ClumsyPilot/tests/verify_path_smoothing_service.ps1 +++ b/.task8-sweep/tests/verify_path_smoothing_service.ps1 @@ -152,6 +152,7 @@ $forward = [Enum]::Parse($directionType, 'Forward') $cubicBSpline = [Enum]::Parse($methodType, 'CubicBSpline') $localCubicBezier = [Enum]::Parse($methodType, 'LocalCubicBezier') $piecewiseQuintic = [Enum]::Parse($methodType, 'PiecewiseQuintic') +$localG2Quintic = [Enum]::Parse($methodType, 'LocalG2Quintic') $coarseAnchor = [Enum]::Parse($coarsePointSourceType, 'Start') # The public method registry must retain every stable enum-to-algorithm mapping. @@ -163,6 +164,12 @@ foreach ($method in @($cubicBSpline, $localCubicBezier, $piecewiseQuintic)) { Assert-True $result.Diagnostics.Metrics.IsFeasible "A successful $method result must publish feasible diagnostics." } +# Local G2 is a dedicated service pipeline, but must remain available through the same public entrypoint. +$localG2Result = Invoke-Smooth (New-StraightRequest (New-Configuration $localG2Quintic)) +Assert-Equal 'NotNeeded' $localG2Result.Status.ToString() 'A straight Local G2 request must publish its verified baseline as NotNeeded.' +Assert-Equal 'LocalG2Quintic' $localG2Result.Method.ToString() 'Local G2 must retain the selected method.' +Assert-True ($localG2Result.Path.Count -gt 0) 'NotNeeded Local G2 must publish verified geometry.' + # Every configuration scalar is checked before the options snapshot or retry runner starts. $invalidConfigurationCases = @( [PSCustomObject]@{ Name = 'NaN output spacing'; Mutate = { param($c) $c.OutputSpacingMeters = [double]::NaN } }, diff --git a/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 b/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 new file mode 100644 index 0000000..80f7101 --- /dev/null +++ b/.task8-sweep/tests/verify_path_smoothing_svg_csv.ps1 @@ -0,0 +1,188 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { + throw "$Message Expected=$Expected Actual=$Actual" + } +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +function Text-FromCodePoints([int[]]$CodePoints) { + return (($CodePoints | ForEach-Object { [Char]::ConvertFromUtf32($_) }) -join '') +} + +$visualization = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization.' +$styleType = Get-RequiredType ($visualization + 'IeeeFigureStyle') + +foreach ($expectation in @( + @{ Name = 'FigureWidthPoints'; Value = 515.52 }, + @{ Name = 'FigureHeightPoints'; Value = 374.4 }, + @{ Name = 'RawColor'; Value = '#4D4D4D' }, + @{ Name = 'BSplineColor'; Value = '#0072B2' }, + @{ Name = 'BezierColor'; Value = '#D55E00' }, + @{ Name = 'QuinticColor'; Value = '#009E73' }, + @{ Name = 'LimitColor'; Value = '#CC79A7' } +)) { + $field = $styleType.GetField($expectation.Name) + if ($null -eq $field) { throw "IeeeFigureStyle must expose $($expectation.Name)." } + if ($expectation.Value -is [double]) { + Assert-Near $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style." + } + else { + Assert-Equal $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style." + } +} + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$comparison = $root + 'Comparison.' +$facade = $root + 'Facade.' +$coarse = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' +$test = $root + 'Test.' + +$modelType = Get-RequiredType ($visualization + 'SmoothingFigureModel') +$builderType = Get-RequiredType ($visualization + 'SmoothingFigureModelBuilder') +$figureDefinitionType = Get-RequiredType ($visualization + 'SmoothingFigureDefinition') +$figureSetBuilderType = Get-RequiredType ($visualization + 'SmoothingFigureSetBuilder') +$rendererType = Get-RequiredType ($visualization + 'SmoothingSvgRenderer') +$csvWriterType = Get-RequiredType ($visualization + 'SmoothingCsvWriter') +$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult') +$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') +$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService') +$scenarioFactoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') +$gridMapType = Get-RequiredType ($mapping + 'PlanningGridMap') +$poseType = Get-RequiredType ($coarse + 'Pose2D') + +$buildMethod = $builderType.GetMethod('Build', [Type[]]@($comparisonResultType, $gridMapType, $poseType, $poseType, [string], [string])) +if ($null -eq $buildMethod) { throw 'SmoothingFigureModelBuilder must expose Build(comparison, map, start, goal, scenarioId, scenarioLabel).' } +Assert-Equal $modelType $buildMethod.ReturnType 'SmoothingFigureModelBuilder.Build must return the immutable figure model.' +$figureSetBuildMethod = $figureSetBuilderType.GetMethod('Build', [Type[]]@($modelType)) +if ($null -eq $figureSetBuildMethod) { throw 'SmoothingFigureSetBuilder must expose Build(SmoothingFigureModel).' } +$renderMethod = $rendererType.GetMethod('Render', [Type[]]@($figureDefinitionType)) +if ($null -eq $renderMethod) { throw 'SmoothingSvgRenderer must expose Render(SmoothingFigureDefinition).' } +$writeMethod = $csvWriterType.GetMethod('Write', [Type[]]@($modelType)) +if ($null -eq $writeMethod) { throw 'SmoothingCsvWriter must expose Write(SmoothingFigureModel).' } + +$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json' +$fixtureRequests = $scenarioFactoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])).Invoke($null, @((Resolve-Path $fixturePath).Path)) +$comparisonRequest = $fixtureRequests[0] +Assert-Equal $comparisonRequestType $comparisonRequest.GetType() 'The figure test must consume a real immutable comparison request.' +$comparisonResult = [Activator]::CreateInstance($comparisonServiceType).Compare($comparisonRequest, [Threading.CancellationToken]::None) +$coarsePath = $comparisonRequest.SmoothingRequest.CoarsePath +$start = [Activator]::CreateInstance($poseType, @($coarsePath[0].X, $coarsePath[0].Y, $coarsePath[0].Heading)) +$last = $coarsePath[$coarsePath.Count - 1] +$goal = [Activator]::CreateInstance($poseType, @($last.X, $last.Y, $last.Heading)) +$builder = [Activator]::CreateInstance($builderType) +$rawLabel = Text-FromCodePoints @(0x539F, 0x59CB, 0x7C97, 0x8DEF, 0x5F84) +$bSplineLabel = (Text-FromCodePoints @(0x4E09, 0x6B21)) + ' B ' + (Text-FromCodePoints @(0x6837, 0x6761)) +$bezierLabel = (Text-FromCodePoints @(0x5C40, 0x90E8, 0x4E09, 0x6B21)) + ' B' + [Char]::ConvertFromUtf32(0x00E9) + 'zier' +$quinticLabel = Text-FromCodePoints @(0x5206, 0x6BB5, 0x4E94, 0x6B21) +$scenarioId = (Text-FromCodePoints @(0x573A, 0x666F)) + '-escape' +$scenarioLabel = (Text-FromCodePoints @(0x8DEF, 0x5F84)) + ' ' +$model = $buildMethod.Invoke($builder, @($comparisonResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, $scenarioId, $scenarioLabel)) + +$figureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($model)) +$figures = @($figureSet.Figures) +$expectedStems = @('01-coarse-path-overview', '02-all-paths-comparison', '03-cubic-bspline-overview', '04-local-cubic-bezier-overview', '05-piecewise-quintic-overview', '06-curvature-comparison') +Assert-Equal ($expectedStems -join ',') (($figures | ForEach-Object { $_.FileStem }) -join ',') 'Figure set must publish the stable six-file order.' +Assert-True (-not $figures[1].ShowsMapContext) 'All-path comparison must exclude obstacles and endpoint decorations.' +Assert-True ($figures[2].ShowsMapContext -and $figures[2].Series[0].Opacity -lt 1) 'Individual smoother views must retain faded coarse-path context.' +Assert-Near (($figures[0].WorldXMaxMeters - $figures[0].WorldXMinMeters) / ($figures[0].WorldYMaxMeters - $figures[0].WorldYMinMeters)) ($figures[0].PlotWidthPoints / $figures[0].PlotHeightPoints) 'Overhead figures must preserve equal X/Y scale.' + +$renderer = [Activator]::CreateInstance($rendererType) +$svgByStem = @{} +foreach ($figure in $figures) { $svgByStem[$figure.FileStem] = $renderMethod.Invoke($renderer, @($figure)) } +$svg = (($expectedStems | ForEach-Object { $svgByStem[$_] }) -join "`n") +Assert-True ($svg -match ']+viewBox="0 0 515\.52 374\.4"') 'SVG must use the fixed physical view box in typographic points.' +Assert-True $svg.Contains(($scenarioLabel -replace '&', '&' -replace '<', '<' -replace '>', '>')) 'SVG text must XML-escape scenario labels.' +Assert-True $svg.Contains('font-family="SimSun"') 'SVG must emit explicit SimSun Chinese text runs.' +Assert-True $svg.Contains('font-family="Times New Roman"') 'SVG must emit explicit Times New Roman Latin/math text runs.' +foreach ($color in @('#4D4D4D', '#0072B2', '#D55E00', '#009E73', '#CC79A7')) { Assert-True $svg.Contains($color) "SVG must retain the fixed color $color." } +Assert-True $svg.Contains('class="trajectory-point"') 'All visible trajectory samples must render as point markers.' +Assert-True (-not $svg.Contains('stroke-dasharray')) 'Trajectory reports must not use dashed path styles.' +Assert-True (-not $svg.Contains('').Count + if ($count -lt $MinimumCount) { throw "Insufficient public API documentation in $RelativePath ExpectedAtLeast=$MinimumCount Actual=$count" } +} + +$readmePath = Join-Path $MapRoot 'README.md' +if (-not (Test-Path -LiteralPath $readmePath)) { throw "Map README is required: $readmePath" } +$readme = Get-Content -LiteralPath $readmePath -Raw +Assert-Contains $readme 'PlanningMapFactory' 'README must identify the public creation facade.' +Assert-Contains $readme 'IMapObstacleSource' 'README must explain unified obstacle sources.' +Assert-Contains $readme 'PlanningGridMap' 'README must explain the planning snapshot output.' +Assert-Contains $readme 'SourceVersion' 'README must explain cache invalidation versions.' +Assert-Contains $readme 'PlanningMapBuildStatus' 'README must explain explicit map build termination states.' +Assert-Contains $readme 'PNG' 'README must explain debug image output.' +Assert-Contains $readme 'TrapMap' 'README must describe the legacy-map boundary.' + +Assert-DocumentationCount 'PlanningMapFactory.cs' 2 +Assert-DocumentationCount 'PlanningMapRequest.cs' 5 +Assert-DocumentationCount 'PlanningMapBuildResult.cs' 9 +Assert-DocumentationCount 'Core\MapBuildRequest.cs' 4 +Assert-DocumentationCount 'Core\EnvironmentMapBuildResult.cs' 7 +Assert-DocumentationCount 'Core\MapBoundsMm.cs' 12 +Assert-DocumentationCount 'Core\EnvironmentGridMap.cs' 13 +Assert-DocumentationCount 'Core\EnvironmentMapBuilder.cs' 2 +Assert-DocumentationCount 'Obstacles\IMapObstacle.cs' 2 +Assert-DocumentationCount 'Obstacles\CircleObstacle.cs' 6 +Assert-DocumentationCount 'Obstacles\AxisAlignedRectangleObstacle.cs' 7 +Assert-DocumentationCount 'Obstacles\MapObstacleRasterizer.cs' 2 +Assert-DocumentationCount 'Sources\IMapObstacleSource.cs' 5 +Assert-DocumentationCount 'Sources\ManualObstacleSource.cs' 6 +Assert-DocumentationCount 'Sources\TwoLegProjectionInput.cs' 13 +Assert-DocumentationCount 'Sources\TwoLegObstacleSource.cs' 6 +Assert-DocumentationCount 'Sources\TwoLegObstacleProjector.cs' 2 +Assert-DocumentationCount 'Sources\ObstacleProjectionResult.cs' 8 +Assert-DocumentationCount 'Sources\ObstacleSourceStatus.cs' 5 +Assert-DocumentationCount 'Planning\PlanningGridMap.cs' 15 +Assert-DocumentationCount 'Planning\PlanningMapAdapter.cs' 2 +Assert-DocumentationCount 'Planning\ObstacleDistanceField.cs' 2 +Assert-DocumentationCount 'Planning\EuclideanDistanceTransform.cs' 2 +Assert-DocumentationCount 'Planning\PlanningMapCache.cs' 5 +Assert-DocumentationCount 'Test\MovementTest.MapTest.cs' 3 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportRequest.cs' 3 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportResult.cs' 8 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExporter.cs' 6 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageRenderer.cs' 2 +Assert-DocumentationCount 'Test\Visualization\ValidatedPngWriter.cs' 2 + +Write-Output 'Planning map documentation checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_factory.ps1 b/.task8-sweep/tests/verify_planning_map_factory.ps1 new file mode 100644 index 0000000..2f3c763 --- /dev/null +++ b/.task8-sweep/tests/verify_planning_map_factory.ps1 @@ -0,0 +1,88 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } +function Assert-False($Actual, [string]$Message) { if ($Actual) { throw $Message } } +function Assert-Null($Actual, [string]$Message) { if ($null -ne $Actual) { throw $Message } } +function Find-NonPublicInstanceMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Instance,NonPublic')) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } + } + if ($matches) { return $candidate } + } + return $null +} +$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' +$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) +$obstacleType = $assembly.GetType($ns + 'IMapObstacle', $true) +$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) +$circleType = $assembly.GetType($ns + 'CircleObstacle', $true) +$manualType = $assembly.GetType($ns + 'ManualObstacleSource', $true) +$twoLegInputType = $assembly.GetType($ns + 'TwoLegProjectionInput', $true) +$twoLegType = $assembly.GetType($ns + 'TwoLegObstacleSource', $true) +$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) +$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) +$mapResultType = $assembly.GetType($ns + 'PlanningMapBuildResult', $true) +$mapBuildStatusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) +$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $false) +$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $false) +Assert-True ($operationBudgetType -ne $null) 'PlanningOperationBudget must exist for shared planning cancellation and timeout handling.' +Assert-True ($operationStopReasonType -ne $null) 'PlanningOperationStopReason must exist for shared planning cancellation and timeout handling.' +Assert-True $operationStopReasonType.IsEnum 'PlanningOperationStopReason must be an enum.' +foreach ($expectedStopReason in @('None', 'Cancelled', 'TimedOut')) { + Assert-True ($operationStopReasonType.GetEnumNames() -contains $expectedStopReason) "PlanningOperationStopReason must contain $expectedStopReason." +} +Assert-True ($mapResultType.GetProperty('Status') -ne $null) 'PlanningMapBuildResult must expose an explicit build status.' +foreach ($expectedMapStatus in @('Success', 'Failed', 'Cancelled', 'TimedOut')) { + Assert-True ($mapBuildStatusType.GetEnumNames() -contains $expectedMapStatus) "PlanningMapBuildStatus must contain $expectedMapStatus." +} +$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]2000, [single]0, [single]2000)) +$obstacles = [Array]::CreateInstance($obstacleType, 1) +$obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]100)), 0) +$manual = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) +$twoLegInput = [Activator]::CreateInstance($twoLegInputType, @($true, [single]1000, [single]1000, [double]([Math]::PI / 2), [single]100, [single]0, [single]-100, [single]0, [single]40, 'test snapshot')) +$twoLeg = [Activator]::CreateInstance($twoLegType, @('two-leg', [long]1, $false, $twoLegInput)) +$sources = [Array]::CreateInstance($sourceType, 2); $sources.SetValue($twoLeg, 0); $sources.SetValue($manual, 1) +function New-Request($sourceArray, [bool]$allowEmpty) { $request = [Activator]::CreateInstance($requestType); $request.Bounds = $bounds; $request.ResolutionMm = [single]50; $request.ObstacleSources = $sourceArray; $request.AllowExplicitEmptyMap = $allowEmpty; return $request } +$factory = [Activator]::CreateInstance($factoryType) +$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, + @([Threading.CancellationToken], [TimeSpan]), $null) +Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' +$createWithBudget = Find-NonPublicInstanceMethod $factoryType 'Create' @($requestType, $operationBudgetType) +Assert-True ($createWithBudget -ne $null) 'PlanningMapFactory must expose an internal budget-aware Create overload.' +$cancelledSource = New-Object Threading.CancellationTokenSource +$cancelledSource.Cancel() +$cancelledBudget = $budgetConstructor.Invoke(@($cancelledSource.Token, [TimeSpan]::FromSeconds(1))) +$cancelled = $createWithBudget.Invoke($factory, @((New-Request $sources $false), $cancelledBudget)) +Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancelled map construction must retain the cancellation status.' +Assert-False $cancelled.Succeeded 'Cancelled map construction must not succeed.' +Assert-Null $cancelled.Map 'Cancelled map construction must not publish a map.' +Assert-Equal 'None' $cancelled.CacheHit.ToString() 'Cancelled map construction must not publish a cache hit.' +$first = $factory.Create((New-Request $sources $false)) +Assert-True $first.Succeeded 'Mixed source request must build.' +Assert-True $first.Map.PlanningReady 'Applied source geometry must make the map ready.' +Assert-True ($first.Map.IsOccupiedWorld(1.0, 1.1)) 'TwoLeg must project detection-time local coordinates into world metres.' +$second = $factory.Create((New-Request $sources $false)) +Assert-Equal 'Input' $second.CacheHit.ToString() 'Same snapshot must hit complete input cache.' +Assert-True ([object]::ReferenceEquals($first.Map, $second.Map)) 'Same snapshot must return the exact immutable map object.' +$manualVersionTwo = [Activator]::CreateInstance($manualType, @('manual', [long]2, $true, $obstacles)) +$sourcesVersionTwo = [Array]::CreateInstance($sourceType, 2); $sourcesVersionTwo.SetValue($manualVersionTwo, 0); $sourcesVersionTwo.SetValue($twoLeg, 1) +$third = $factory.Create((New-Request $sourcesVersionTwo $false)) +Assert-Equal 'Occupancy' $third.CacheHit.ToString() 'Version change with equal occupancy must reuse occupancy buffers.' +Assert-True ($third.Map.SnapshotId -gt $first.Map.SnapshotId) 'Occupancy reuse must still issue a fresh snapshot id.' +$emptySources = [Array]::CreateInstance($sourceType, 0) +$emptyBlocked = $factory.Create((New-Request $emptySources $false)) +Assert-True (-not $emptyBlocked.Map.PlanningReady) 'Implicit empty map must be blocked.' +$emptyAllowed = $factory.Create((New-Request $emptySources $true)) +Assert-True $emptyAllowed.Map.PlanningReady 'Explicit empty map must be accepted.' +$invalidManual = [Activator]::CreateInstance($manualType, @('invalid', [long]0, $true, $null)) +$invalidSources = [Array]::CreateInstance($sourceType, 1); $invalidSources.SetValue($invalidManual, 0) +$invalid = $factory.Create((New-Request $invalidSources $false)) +Assert-True (-not $invalid.Succeeded) 'Required invalid source must reject the full build.' +Write-Output 'Planning map factory checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_image.ps1 b/.task8-sweep/tests/verify_planning_map_image.ps1 new file mode 100644 index 0000000..b674c57 --- /dev/null +++ b/.task8-sweep/tests/verify_planning_map_image.ps1 @@ -0,0 +1,31 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' +$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) +$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) +$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) +$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) +$imageRequestType = $assembly.GetType($ns + 'PlanningMapImageExportRequest', $true) +$exporterType = $assembly.GetType($ns + 'PlanningMapImageExporter', $true) +$mapRequest = [Activator]::CreateInstance($requestType) +$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]1000, [single]0, [single]1000)) +$mapRequest.ResolutionMm = [single]50 +$mapRequest.ObstacleSources = [Array]::CreateInstance($sourceType, 0) +$mapRequest.AllowExplicitEmptyMap = $true +$map = [Activator]::CreateInstance($factoryType).Create($mapRequest).Map +$imageRequest = [Activator]::CreateInstance($imageRequestType) +$imageRequest.Map = $map +$imageRequest.OutputRootDirectory = (Join-Path $PSScriptRoot '..\obj\planning_map_image_test') +$export = $exporterType.GetMethod('ExportIfEnabled').Invoke($null, @($true, $imageRequest)) +Assert-True $export.Saved ('Image export failed: ' + $export.Message) +Assert-True (Test-Path -LiteralPath $export.FilePath -PathType Leaf) 'Image output is missing.' +$png = [IO.File]::ReadAllBytes($export.FilePath) +Assert-True ($png.Length -gt 45) 'PNG is too small.' +Assert-True ($png[0] -eq 137 -and $png[1] -eq 80 -and $png[2] -eq 78 -and $png[3] -eq 71) 'PNG signature is invalid.' +Assert-True ([Text.Encoding]::ASCII.GetString($png, 12, 4) -eq 'IHDR') 'PNG must begin with IHDR.' +Assert-True ([Text.Encoding]::ASCII.GetString($png, $png.Length - 8, 4) -eq 'IEND') 'PNG must end with IEND.' +$source = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\Visualization\PlanningMapImageExporter.cs') -Raw -Encoding UTF8 +Assert-True ($source -notmatch 'GridMapData|TrapMapVehiclePose|TwoLegDetect') 'New exporter must consume only PlanningGridMap.' +Write-Output 'Planning map image checks passed.' diff --git a/.task8-sweep/tests/verify_planning_map_test_config.ps1 b/.task8-sweep/tests/verify_planning_map_test_config.ps1 new file mode 100644 index 0000000..ad52397 --- /dev/null +++ b/.task8-sweep/tests/verify_planning_map_test_config.ps1 @@ -0,0 +1,23 @@ +param([string]$SourcePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\MovementTest.MapTest.cs')) + +$ErrorActionPreference = 'Stop' +$source = Get-Content -LiteralPath $SourcePath -Raw + +function Assert-Contains([string]$Expected, [string]$Message) { + if (-not $source.Contains($Expected)) { throw "$Message Missing=$Expected" } +} + +Assert-Contains 'EnableTerminalDebugLog' 'MapTest must expose a terminal logging switch.' +Assert-Contains 'SavePng' 'MapTest must expose a PNG export switch.' +$begin = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5F00, 0x59CB) +$sources = -join [char[]](0x969C, 0x788D, 0x7269, 0x6765) +$projection = -join [char[]](0x6295, 0x5F71, 0x7ED3, 0x679C) +$snapshot = -join [char[]](0x5730, 0x56FE, 0x5FEB, 0x7167) +$end = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5B8C, 0x6210) +Assert-Contains 'FormatSourceStatus' 'MapTest must translate projection status for terminal output.' +Assert-Contains 'FormatCacheHit' 'MapTest must translate cache hits for terminal output.' +Assert-Contains 'FormatImageResult' 'MapTest must translate image export results for terminal output.' +if ($source.Contains('PLANNING_MAP_CREATE_BEGIN')) { throw 'MapTest must not expose English terminal log markers.' } +Assert-Contains 'PlanningMapFactory' 'MapTest must continue using the public map facade.' + +Write-Output 'Planning map MovementTest configuration checks passed.' diff --git a/ClumsyPilot/ClumsyPilot.csproj b/ClumsyPilot/ClumsyPilot.csproj index 9e3bc8f..cb38160 100644 --- a/ClumsyPilot/ClumsyPilot.csproj +++ b/ClumsyPilot/ClumsyPilot.csproj @@ -8,21 +8,26 @@ + + + - + ref\CommonUsage.dll @@ -43,7 +48,6 @@ ref\RefFundamentalLib.dll - + + + + + + + + + diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs new file mode 100644 index 0000000..74b55ef --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs @@ -0,0 +1,63 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +///

最终粗路径上的一个稠密连续点;长度、净空和位置使用 m,航向使用 rad,曲率使用 1/m。 +public sealed class CoarsePathPoint +{ + /// + /// 创建粗路径点。 + /// 参数:xMeters、yMeters 为世界坐标 m;headingRadians 与 unwrappedHeadingRadians 为航向 rad;arcLengthMeters 和 bodyClearanceMeters 为 m;vehicleCurvaturePerMeter 为 1/m。 + /// + public CoarsePathPoint( + double xMeters, + double yMeters, + double headingRadians, + double unwrappedHeadingRadians, + double arcLengthMeters, + TravelDirection direction, + double vehicleCurvaturePerMeter, + double bodyClearanceMeters, + bool isGearSwitchPoint, + CoarsePathPointSource source) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + UnwrappedHeading = unwrappedHeadingRadians; + ArcLength = arcLengthMeters; + Direction = direction; + VehicleCurvature = vehicleCurvaturePerMeter; + BodyClearance = bodyClearanceMeters; + IsGearSwitchPoint = isGearSwitchPoint; + Source = source; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 归一化后可用于几何查询的车头航向,单位 rad。 + public double Heading { get; } + + /// 跨越 ±π 后仍连续的车头航向,单位 rad。 + public double UnwrappedHeading { get; } + + /// 从路径起点累计的弧长,单位 m;必须非负且不递减。 + public double ArcLength { get; } + + /// 从上一点运动到当前点所在方向段的行驶方向。 + public TravelDirection Direction { get; } + + /// 车辆在此点采用的恒曲率原语曲率,单位 1/m。 + public double VehicleCurvature { get; } + + /// 扩大车体到障碍物的保守净空下界,单位 m。 + public double BodyClearance { get; } + + /// 此点是否为新方向段开始的换向点。 + public bool IsGearSwitchPoint { get; } + + /// 此点由起点、普通原语或终点截断产生的来源。 + public CoarsePathPointSource Source { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs new file mode 100644 index 0000000..1e4ad26 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径点在搜索与原语重建中的产生来源。 +public enum CoarsePathPointSource +{ + /// 请求提供的起始位姿。 + Start, + + /// 未截断恒曲率运动原语的内部积分点。 + MotionPrimitive, + + /// 首次满足目标容差而在原语内部截断的终点积分点。 + GoalTruncation, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs new file mode 100644 index 0000000..351c690 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 终点候选最后一个连续运动段的方向约束。 +public enum GoalDirectionConstraint +{ + /// 不限制进入终点的运动方向。 + Any, + + /// 必须以前进方向进入终点。 + Forward, + + /// 必须以倒车方向进入终点。 + Reverse, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs new file mode 100644 index 0000000..dc361f0 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs @@ -0,0 +1,83 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// Hybrid A* 粗路径搜索的可调配置。 +/// 长度和距离使用 m,航向使用 rad,曲率使用 1/m;各项有效范围由规划器在执行前校验。 +/// +public sealed class HybridAStarConfiguration +{ + /// 创建采用 P0 固定安全边界和代价权重的默认配置。 + public HybridAStarConfiguration() + { + PrimitiveLengthMeters = 0.50d; + IntegrationStepMeters = 0.05d; + MaximumCollisionCheckStepMeters = 0.025d; + HeadingResolutionRadians = Math.PI / 36d; + CurvatureLevelCount = 5; + GoalPositionToleranceMeters = 0.15d; + GoalHeadingToleranceRadians = Math.PI / 36d; + MaximumExpandedNodes = 200000; + SearchTimeout = TimeSpan.FromSeconds(5d); + HeuristicWeight = 1d; + ReverseCostMultiplier = 1.5d; + GearSwitchPenaltyMeters = 1d; + CurvatureMagnitudeWeight = 0.10d; + CurvatureChangePenaltyMetersPerLevel = 0.05d; + ClearanceCostWeight = 0.20d; + ClearanceCostDistanceMeters = 0.50d; + AllowReverse = true; + } + + /// 单个恒曲率原语的最大行驶长度,单位 m。 + public double PrimitiveLengthMeters { get; set; } + + /// 原语内部输出积分点之间允许的最大弧长,单位 m。 + public double IntegrationStepMeters { get; set; } + + /// 连续碰撞检查允许的最大车辆中心位移,单位 m;实际值还受地图分辨率限制。 + public double MaximumCollisionCheckStepMeters { get; set; } + + /// 搜索状态离散所使用的航向格宽,单位 rad。 + public double HeadingResolutionRadians { get; set; } + + /// 从最大负曲率到最大正曲率的离散曲率等级数。 + public int CurvatureLevelCount { get; set; } + + /// 终点位置允许的欧氏距离误差,单位 m。 + public double GoalPositionToleranceMeters { get; set; } + + /// 终点车头航向允许的最小环形角度误差,单位 rad。 + public double GoalHeadingToleranceRadians { get; set; } + + /// 单次搜索允许扩展的最大节点数。 + public int MaximumExpandedNodes { get; set; } + + /// 单次搜索允许消耗的最长时间;超出后返回 + public TimeSpan SearchTimeout { get; set; } + + /// 二维绕障启发式的权重;1 表示不额外放大。 + public double HeuristicWeight { get; set; } + + /// 倒车原语长度代价相对前进原语的倍率。 + public double ReverseCostMultiplier { get; set; } + + /// 相邻原语发生换向时增加的等效距离代价,单位 m。 + public double GearSwitchPenaltyMeters { get; set; } + + /// 曲率绝对值对应的无量纲代价权重。 + public double CurvatureMagnitudeWeight { get; set; } + + /// 相邻曲率等级每变化一级增加的等效距离代价,单位 m。 + public double CurvatureChangePenaltyMetersPerLevel { get; set; } + + /// 车体保守净空不足时增加的无量纲代价权重。 + public double ClearanceCostWeight { get; set; } + + /// 计算净空代价时视为足够安全的车体保守净空,单位 m。 + public double ClearanceCostDistanceMeters { get; set; } + + /// 是否允许生成倒车原语;false 时搜索只生成前进原语。 + public bool AllowReverse { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs new file mode 100644 index 0000000..5a6a0e9 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs @@ -0,0 +1,37 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径中方向一致的一段连续点范围;索引两端均包含在段内。 +public sealed class PathSegment +{ + /// + /// 创建方向分段。 + /// 参数:segmentIndex 为从零开始的段序号;startIndex 与 endIndex 为 的包含式索引;两个换向标记描述段首或段尾是否位于换向对。 + /// + public PathSegment(int segmentIndex, TravelDirection direction, int startIndex, int endIndex, bool startsAtGearSwitch, bool endsAtGearSwitch) + { + SegmentIndex = segmentIndex; + Direction = direction; + StartIndex = startIndex; + EndIndex = endIndex; + StartsAtGearSwitch = startsAtGearSwitch; + EndsAtGearSwitch = endsAtGearSwitch; + } + + /// 从零开始的方向段序号。 + public int SegmentIndex { get; } + + /// 此段所有连续运动点对应的行驶方向。 + public TravelDirection Direction { get; } + + /// 此段在 中的起始索引,包含该点。 + public int StartIndex { get; } + + /// 此段在 中的结束索引,包含该点。 + public int EndIndex { get; } + + /// 此段首点是否为换向后保留的新方向点。 + public bool StartsAtGearSwitch { get; } + + /// 此段尾点是否紧邻下一方向段的换向对。 + public bool EndsAtGearSwitch { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs new file mode 100644 index 0000000..8ec8ea3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs @@ -0,0 +1,65 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 一次规划的只读统计与终止说明;长度和净空单位为 m。 +public sealed class PlanningDiagnostics +{ + /// + /// 创建规划统计快照。 + /// 参数中的节点数量均为非负计数;pathLengthMeters 和 minimumBodyClearanceMeters 单位为 m;elapsed 为总耗时;pathSearchElapsed 为地图和起终点预检通过后的路径产出耗时;terminationReason 为可读终止说明,可为 null。 + /// + public PlanningDiagnostics( + int expandedNodeCount = 0, + int generatedNodeCount = 0, + int reopenedNodeCount = 0, + int staleOpenListEntryCount = 0, + int peakOpenListCount = 0, + double pathLengthMeters = 0d, + double minimumBodyClearanceMeters = 0d, + TimeSpan elapsed = default(TimeSpan), + string terminationReason = null, + TimeSpan pathSearchElapsed = default(TimeSpan)) + { + ExpandedNodeCount = expandedNodeCount; + GeneratedNodeCount = generatedNodeCount; + ReopenedNodeCount = reopenedNodeCount; + StaleOpenListEntryCount = staleOpenListEntryCount; + PeakOpenListCount = peakOpenListCount; + PathLengthMeters = pathLengthMeters; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + Elapsed = elapsed; + PathSearchElapsed = pathSearchElapsed; + TerminationReason = terminationReason ?? string.Empty; + } + + /// 从 Open List 取出并真正扩展的节点数量。 + public int ExpandedNodeCount { get; } + + /// 生成并尝试加入搜索状态的节点数量。 + public int GeneratedNodeCount { get; } + + /// 以严格更小代价到达同一离散键而重新打开的节点数量。 + public int ReopenedNodeCount { get; } + + /// 从 Open List 取出后因已有更优条目而丢弃的陈旧堆条目数量。 + public int StaleOpenListEntryCount { get; } + + /// 搜索期间 Open List 同时容纳的最大有效或待丢弃条目数量。 + public int PeakOpenListCount { get; } + + /// 成功路径的累计弧长,单位 m;失败结果通常为 0。 + public double PathLengthMeters { get; } + + /// 成功路径所有点中车体保守净空下界的最小值,单位 m;失败结果通常为 0。 + public double MinimumBodyClearanceMeters { get; } + + /// 从规划入口到返回结果的总耗时。 + public TimeSpan Elapsed { get; } + + /// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时;不含建图,搜索前失败时为零。 + public TimeSpan PathSearchElapsed { get; } + + /// 面向调用方的终止原因;成功时可为空字符串。 + public string TerminationReason { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs new file mode 100644 index 0000000..057789f --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs @@ -0,0 +1,41 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 已建规划地图上的一次 Hybrid A* 请求。 +/// 本对象只接受不可变 快照,不包含地图构建器、传感器、UI 或调试对象。 +/// +public sealed class PlanningRequest +{ + /// 创建空规划请求。调用规划器前必须提供地图、起点、终点、车辆和配置。 + public PlanningRequest() + { + StartVehicleCurvature = 0d; + GoalDirection = GoalDirectionConstraint.Any; + } + + /// 本次搜索唯一允许查询的不可变规划地图快照;位置查询单位为 m。 + public PlanningGridMap Map { get; set; } + + /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; set; } + + /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 + public Pose2D Goal { get; set; } + + /// 车辆几何、余量和曲率限制;尺寸单位 m,曲率单位 1/m。 + public VehicleParameters Vehicle { get; set; } + + /// 搜索步长、离散、终点容差、代价和资源上限配置。 + public HybridAStarConfiguration Configuration { get; set; } + + /// 车辆起步时的转向曲率,单位 1/m;默认值为 0。 + public double StartVehicleCurvature { get; set; } + + /// 起步方向约束;null 表示可从前进或倒车开始。 + public TravelDirection? StartDirection { get; set; } + + /// 目标进入方向约束;默认值为 + public GoalDirectionConstraint GoalDirection { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs new file mode 100644 index 0000000..c69548f --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 一次粗路径规划的最终不可变结果。 +/// 只有 时才携带非空路径与方向分段;其他状态始终返回空只读集合。 +/// +public sealed class PlanningResult +{ + private static readonly IReadOnlyList EmptyPath = new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptySegments = new ReadOnlyCollection(new List()); + + private PlanningResult(PlanningStatus status, PlanningDiagnostics diagnostics, IReadOnlyList path, IReadOnlyList segments) + { + Status = status; + Diagnostics = diagnostics ?? new PlanningDiagnostics(terminationReason: "未提供诊断信息。"); + Path = path; + Segments = segments; + } + + /// 规划最终状态;只有 可以发布路径。 + public PlanningStatus Status { get; } + + /// 节点、耗时、路径长度、净空和终止原因统计;始终非空。 + public PlanningDiagnostics Diagnostics { get; } + + /// 成功时的稠密粗路径;失败时为不可修改的空集合。 + public IReadOnlyList Path { get; } + + /// 成功时覆盖 的包含式方向分段;失败时为不可修改的空集合。 + public IReadOnlyList Segments { get; } + + /// + /// 创建成功结果。 + /// 参数:path 与 segments 必须均为非空;diagnostics 为本次规划的统计快照。参数不符合要求时抛出 ,防止以成功状态发布不完整路径。 + /// + public static PlanningResult Success(IReadOnlyList path, IReadOnlyList segments, PlanningDiagnostics diagnostics) + { + if (path == null || path.Count == 0) + throw new ArgumentException("Successful planning results require a non-empty path.", nameof(path)); + if (segments == null || segments.Count == 0) + throw new ArgumentException("Successful planning results require non-empty segments.", nameof(segments)); + return new PlanningResult(PlanningStatus.Success, diagnostics, CopyReadOnly(path), CopyReadOnly(segments)); + } + + /// + /// 创建失败、取消或资源受限结果。 + /// 参数:status 不能为 ;diagnostics 会原样保留。返回结果的路径与分段始终为空只读集合。 + /// + public static PlanningResult Failure(PlanningStatus status, PlanningDiagnostics diagnostics) + { + if (status == PlanningStatus.Success) + throw new ArgumentException("Use Success to create a successful planning result.", nameof(status)); + return new PlanningResult(status, diagnostics, EmptyPath, EmptySegments); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs new file mode 100644 index 0000000..26d5fea --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs @@ -0,0 +1,56 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 粗路径规划的最终状态;除 外均不发布路径或方向分段。 +public enum PlanningStatus +{ + /// 已得到并通过最终连续碰撞复核的完整粗路径。 + Success, + + /// 在搜索扩展检查点收到取消请求。 + Cancelled, + + /// 请求对象或其必要成员为空,或包含不符合基本契约的值。 + InvalidRequest, + + /// 请求提供的地图对象不满足规划器的结构要求。 + InvalidMap, + + /// 地图快照尚未准备好参与规划;应查看地图的阻止原因。 + MapNotReady, + + /// 车辆尺寸、安全余量或曲率限制无效。 + InvalidVehicleParameters, + + /// 原语、离散、代价、容差或资源限制配置无效。 + InvalidCurvatureConfiguration, + + /// 起始车辆几何中心或扩大车体不在地图范围内。 + StartOutsideMap, + + /// 起始扩大车体与地图障碍物相交或擦边。 + StartInCollision, + + /// 目标车辆几何中心或扩大车体不在地图范围内。 + GoalOutsideMap, + + /// 目标扩大车体与地图障碍物相交或擦边。 + GoalInCollision, + + /// 搜索达到 限制。 + SearchTimeout, + + /// 搜索达到 限制。 + SearchNodeLimitExceeded, + + /// Open List 已耗尽,或二维启发式证明目标不可达。 + NoFeasiblePath, + + /// 搜索成功节点无法按父链回溯为完整路径。 + BacktrackingFailed, + + /// 回溯路径未通过连续碰撞、终点或输出不变量复核。 + FinalValidationFailed, + + /// 规划内部发生未预期错误;不会发布部分路径。 + InternalError, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs new file mode 100644 index 0000000..77af135 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs @@ -0,0 +1,28 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 粗路径规划使用的二维连续位姿。 +/// 位置以世界坐标 m 表示,航向以 rad 表示;此值对象不在构造时归一化航向,调用方可保留展开航向。 +/// +public sealed class Pose2D +{ + /// + /// 创建二维位姿。 + /// 参数:xMeters、yMeters 为世界坐标,单位 m;headingRadians 为车头航向,单位 rad。 + /// + public Pose2D(double xMeters, double yMeters, double headingRadians) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 车头航向,单位 rad;可以是已展开的连续航向。 + public double Heading { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs new file mode 100644 index 0000000..8bf17dc --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// 连续运动段的行驶方向。 +public enum TravelDirection +{ + /// 沿车辆车头方向前进。 + Forward, + + /// 与车辆车头方向相反地倒车。 + Reverse, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs new file mode 100644 index 0000000..286f928 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs @@ -0,0 +1,28 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 车辆几何与运动学参数。 +/// 所有几何尺寸均以车辆几何中心为 参考点,单位为 m;曲率单位为 1/m。 +/// +public sealed class VehicleParameters +{ + /// 创建空车辆参数。调用规划器前必须填写有效的几何尺寸和至少一种曲率限制。 + public VehicleParameters() + { + } + + /// 车辆本体长度,单位 m;不含 + public double LengthMeters { get; set; } + + /// 车辆本体宽度,单位 m;不含 + public double WidthMeters { get; set; } + + /// 碰撞检查时加在车体四周的安全余量,单位 m;不会写入地图。 + public double SafetyMarginMeters { get; set; } + + /// 车辆允许的最大绝对曲率,单位 1/m;null 表示由 提供限制。 + public double? MaximumCurvaturePerMeter { get; set; } + + /// 车辆允许的最小转弯半径,单位 m;null 表示由 提供限制。 + public double? MinimumTurningRadiusMeters { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs new file mode 100644 index 0000000..78b4685 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs @@ -0,0 +1,45 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次从地图输入到 Hybrid A* 粗路径输出的完整业务请求。 +/// 地图边界、分辨率和障碍物几何位于 中并使用 mm;位姿和车辆几何使用 m,航向使用 rad,曲率使用 1/m。 +/// +public sealed class CoarsePathPlanningJob +{ + /// 创建默认目标方向、起步曲率和关闭调试旁路的业务请求。 + public CoarsePathPlanningJob() + { + StartVehicleCurvature = 0d; + GoalDirection = GoalDirectionConstraint.Any; + DebugOptions = new PlanningDebugOptions(); + } + + /// 本次唯一建图输入;边界、分辨率和障碍物几何均遵循 Map 模块的 mm 契约。 + public PlanningMapRequest MapRequest { get; set; } + + /// 车辆几何中心的起始位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; set; } + + /// 车辆几何中心的目标位姿;位置单位 m,航向单位 rad。 + public Pose2D Goal { get; set; } + + /// 车辆尺寸、安全余量和曲率限制;尺寸单位 m,曲率单位 1/m。 + public VehicleParameters Vehicle { get; set; } + + /// 原语、离散、容差、代价和资源上限配置。 + public HybridAStarConfiguration Configuration { get; set; } + + /// 车辆起步曲率,单位 1/m;默认值为 0。 + public double StartVehicleCurvature { get; set; } + + /// 起步方向约束;null 表示可从前进或倒车开始。 + public TravelDirection? StartDirection { get; set; } + + /// 目标进入方向约束;默认值为 + public GoalDirectionConstraint GoalDirection { get; set; } + + /// 可选调试旁路配置;默认关闭且使用空接收器,不参与地图或路径计算。 + public PlanningDebugOptions DebugOptions { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs new file mode 100644 index 0000000..b96208d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次粗路径业务编排的不可变结果。 +/// 始终同时保留地图构建结果和规划结果;调试旁路消息只用于诊断,不会修改地图或规划内容。 +/// +public sealed class CoarsePathPlanningJobResult +{ + /// + /// 创建业务编排结果。 + /// 参数:mapResult 和 planningResult 均不能为空;debugDiagnostics 可为 null,返回时会转换为只读字符串集合。 + /// + public CoarsePathPlanningJobResult(PlanningMapBuildResult mapResult, PlanningResult planningResult, + IReadOnlyList debugDiagnostics = null) + { + MapResult = mapResult ?? throw new ArgumentNullException(nameof(mapResult)); + PlanningResult = planningResult ?? throw new ArgumentNullException(nameof(planningResult)); + DebugDiagnostics = CopyDiagnostics(debugDiagnostics); + } + + /// 本次调用的地图创建结果;失败时读取 + public PlanningMapBuildResult MapResult { get; } + + /// 本次调用的粗路径结果;地图创建失败时为无路径的失败状态。 + public PlanningResult PlanningResult { get; } + + /// 调试旁路的非致命诊断信息;为空时表示未启用、未发生异常或无额外调试消息。 + public IReadOnlyList DebugDiagnostics { get; } + + private static IReadOnlyList CopyDiagnostics(IReadOnlyList source) + { + if (source == null || source.Count == 0) + return new ReadOnlyCollection(new List()); + + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index] ?? string.Empty); + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs new file mode 100644 index 0000000..a4e5926 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 从 到 Hybrid A* 粗路径的一次调用服务。 +/// 服务生命周期内长期持有同一个地图工厂和规划器,以保留地图缓存并避免将 UI、传感器或调试依赖带入规划核心。 +/// +public sealed class CoarsePathPlanningService +{ + private readonly PlanningMapFactory _mapFactory; + private readonly HybridAStarPlanner _planner; + + /// 创建长期复用默认地图工厂与 Hybrid A* 规划器的服务。 + public CoarsePathPlanningService() + : this(new PlanningMapFactory(), new HybridAStarPlanner()) + { + } + + internal CoarsePathPlanningService(PlanningMapFactory mapFactory, HybridAStarPlanner planner) + { + _mapFactory = mapFactory ?? throw new ArgumentNullException(nameof(mapFactory)); + _planner = planner ?? throw new ArgumentNullException(nameof(planner)); + } + + /// + /// 按固定顺序创建地图并执行一次 Hybrid A* 粗路径规划。 + /// 参数:job 的地图输入使用 mm,位姿和车辆尺寸使用 m,航向使用 rad,曲率使用 1/m;cancellationToken 会传递给搜索阶段。 + /// 返回:始终同时保留地图创建结果和规划结果;地图创建失败时不会启动搜索,并返回 的空路径结果。 + /// + public CoarsePathPlanningJobResult Plan(CoarsePathPlanningJob job, + CancellationToken cancellationToken = default(CancellationToken)) + { + PlanningOperationBudget budget = CreateBudget(job, cancellationToken); + PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); + if (!mapResult.Succeeded || mapResult.Map == null) + { + PlanningResult mapFailure = PlanningResult.Failure(MapFailureStatus(mapResult), + new PlanningDiagnostics(elapsed: budget.Elapsed, terminationReason: BuildMapFailureReason(mapResult))); + return PublishDebug(job, mapResult, mapFailure); + } + + var request = new PlanningRequest + { + Map = mapResult.Map, + Start = job.Start, + Goal = job.Goal, + Vehicle = job.Vehicle, + Configuration = job.Configuration, + StartVehicleCurvature = job.StartVehicleCurvature, + StartDirection = job.StartDirection, + GoalDirection = job.GoalDirection, + }; + PlanningResult planningResult = _planner.Plan(request, budget); + return PublishDebug(job, mapResult, planningResult); + } + + private static PlanningOperationBudget CreateBudget(CoarsePathPlanningJob job, CancellationToken cancellationToken) + { + HybridAStarConfiguration configuration = job == null ? null : job.Configuration; + return configuration != null && configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, configuration.SearchTimeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + } + + private static PlanningStatus MapFailureStatus(PlanningMapBuildResult mapResult) + { + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) return PlanningStatus.Cancelled; + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) return PlanningStatus.SearchTimeout; + return PlanningStatus.InvalidMap; + } + + private static CoarsePathPlanningJobResult PublishDebug(CoarsePathPlanningJob job, PlanningMapBuildResult mapResult, + PlanningResult planningResult) + { + var diagnostics = new List(); + PlanningDebugOptions options = job == null ? null : job.DebugOptions; + if (options != null && options.Enabled) + { + IPlanningDebugSink sink = options.Sink ?? NullPlanningDebugSink.Instance; + try + { + sink.Publish(mapResult, planningResult); + } + catch (Exception exception) + { + diagnostics.Add("调试旁路发布失败:" + exception.GetType().Name + "。" + exception.Message); + } + } + + return new CoarsePathPlanningJobResult(mapResult, planningResult, diagnostics); + } + + private static string BuildMapFailureReason(PlanningMapBuildResult mapResult) + { + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.Cancelled) + return "规划地图创建已取消,未启动 Hybrid A* 搜索。"; + if (mapResult != null && mapResult.Status == PlanningMapBuildStatus.TimedOut) + return "规划地图创建已超时,未启动 Hybrid A* 搜索。"; + string reason = mapResult == null ? string.Empty : mapResult.FailureReason; + return string.IsNullOrEmpty(reason) ? "规划地图创建失败,未启动 Hybrid A* 搜索。" : + "规划地图创建失败,未启动 Hybrid A* 搜索:" + reason; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs new file mode 100644 index 0000000..3f6399e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs @@ -0,0 +1,29 @@ +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 接收一次地图构建和粗路径规划完成后的旁路调试数据。 +/// 实现不得修改输入对象;服务会隔离实现抛出的异常,调试失败不会改变规划结果。 +/// +public interface IPlanningDebugSink +{ + /// + /// 发布本次编排得到的地图和规划结果。 + /// 参数:mapResult 为地图创建结果;planningResult 为对应的完整或失败规划结果;二者均不可由接收方修改。 + /// + void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult); +} + +internal sealed class NullPlanningDebugSink : IPlanningDebugSink +{ + internal static readonly NullPlanningDebugSink Instance = new NullPlanningDebugSink(); + + private NullPlanningDebugSink() + { + } + + public void Publish(PlanningMapBuildResult mapResult, PlanningResult planningResult) + { + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs new file mode 100644 index 0000000..a022f20 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs @@ -0,0 +1,23 @@ +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; + +/// +/// 一次粗路径编排的可选调试旁路配置。 +/// 调试开关和接收器不参与地图输入指纹、缓存键、搜索状态或路径结果。 +/// +public sealed class PlanningDebugOptions +{ + /// 创建默认关闭并使用空接收器的调试配置。 + public PlanningDebugOptions() + { + Enabled = false; + Sink = NullPlanningDebugSink.Instance; + } + + /// 是否在本次编排结束后向 发布旁路数据;默认值为 false。 + public bool Enabled { get; set; } + + /// + /// 接收地图和规划结果的旁路对象;默认为空实现。赋值为 null 时服务仍会使用空实现,且不会影响主流程。 + /// + public IPlanningDebugSink Sink { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs new file mode 100644 index 0000000..697a488 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs @@ -0,0 +1,264 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Output; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath; + +/// +/// 已建 上 Hybrid A* 粗路径规划的下层门面。 +/// 本类不建图、不读取 UI 或传感器;只有路径经回溯、装配和最终连续复核后才发布成功结果。 +/// +public sealed class HybridAStarPlanner +{ + private readonly HybridAStarSearch _search; + private readonly PathBacktracker _backtracker; + private readonly CoarsePathAssembler _assembler; + private readonly CoarsePathValidator _validator; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认搜索、回溯、装配和最终复核组件的规划器。 + public HybridAStarPlanner() + : this(new HybridAStarSearch(), new PathBacktracker(), new CoarsePathAssembler(), new CoarsePathValidator(), + new FootprintCollisionChecker()) + { + } + + internal HybridAStarPlanner(HybridAStarSearch search, PathBacktracker backtracker, CoarsePathAssembler assembler, + CoarsePathValidator validator, FootprintCollisionChecker collisionChecker) + { + _search = search ?? throw new ArgumentNullException(nameof(search)); + _backtracker = backtracker ?? throw new ArgumentNullException(nameof(backtracker)); + _assembler = assembler ?? throw new ArgumentNullException(nameof(assembler)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 在请求提供的不可变规划地图上执行一次 Hybrid A* 粗路径规划。 + /// 参数:request 的位置单位为 m、航向单位为 rad、曲率单位为 1/m;cancellationToken 会在搜索扩展检查点取消。 + /// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 携带完整路径和分段。 + /// + public PlanningResult Plan(PlanningRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, request.Configuration.SearchTimeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + return Plan(request, budget); + } + + /// 使用门面传入的共享预算执行预检、搜索和最终路径复核。 + internal PlanningResult Plan(PlanningRequest request, PlanningOperationBudget budget) + { + Stopwatch pathSearchStopwatch = null; + try + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return Failure(ToPlanningStatus(stopReason), budget, "规划在开始前已停止。", null); + + PlanningStatus preflightStatus = ValidatePreflight(request, out string preflightReason); + if (preflightStatus != PlanningStatus.Success) + return Failure(preflightStatus, budget, preflightReason, null); + + if (!IsFootprintInsideMap(request.Start, request.Map, request.Vehicle)) + return Failure(PlanningStatus.StartOutsideMap, budget, "起始扩大车体不完全位于地图边界内。", null); + if (!_collisionChecker.IsPoseCollisionFree(request.Start, request.Map, request.Vehicle, 0d, out _)) + return Failure(PlanningStatus.StartInCollision, budget, "起始扩大车体与障碍物相交或擦边。", null); + if (!IsFootprintInsideMap(request.Goal, request.Map, request.Vehicle)) + return Failure(PlanningStatus.GoalOutsideMap, budget, "目标扩大车体不完全位于地图边界内。", null); + if (!_collisionChecker.IsPoseCollisionFree(request.Goal, request.Map, request.Vehicle, 0d, out _)) + return Failure(PlanningStatus.GoalInCollision, budget, "目标扩大车体与障碍物相交或擦边。", null); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return Failure(ToPlanningStatus(stopReason), budget, "规划在搜索前已停止。", null); + pathSearchStopwatch = Stopwatch.StartNew(); + HybridAStarSearchResult searchResult = _search.Search(request, budget); + if (searchResult == null) + return Failure(PlanningStatus.InternalError, budget, "搜索器未返回结果。", null, pathSearchStopwatch); + if (searchResult.Status != PlanningStatus.Success) + return Failure(searchResult.Status, budget, + BuildSearchFailureReason(searchResult, request.Configuration), searchResult, pathSearchStopwatch); + + if (!_backtracker.TryBacktrack(searchResult, request, out BacktrackedPath backtrackedPath, out string backtrackingReason)) + return Failure(PlanningStatus.BacktrackingFailed, budget, backtrackingReason, searchResult, pathSearchStopwatch); + if (!_assembler.TryAssemble(backtrackedPath, request, out var path, out var segments, out string assemblyReason)) + return Failure(PlanningStatus.FinalValidationFailed, budget, assemblyReason, searchResult, pathSearchStopwatch); + if (!_validator.TryValidate(path, segments, request, out double minimumClearanceMeters, out string validationReason)) + return Failure(PlanningStatus.FinalValidationFailed, budget, validationReason, searchResult, pathSearchStopwatch); + + double pathLengthMeters = path[path.Count - 1].ArcLength; + return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, + minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); + } + catch (Exception exception) + { + string reason = "规划内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return Failure(PlanningStatus.InternalError, budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), + reason, null, pathSearchStopwatch); + } + } + + private static PlanningStatus ValidatePreflight(PlanningRequest request, out string failureReason) + { + failureReason = string.Empty; + if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || + !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || + !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) + { + failureReason = "规划请求缺少必要对象或包含非法数值。"; + return PlanningStatus.InvalidRequest; + } + + PlanningGridMap map = request.Map; + if (map.Bounds == null || map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters)) + { + failureReason = "规划地图结构无效。"; + return PlanningStatus.InvalidMap; + } + if (!map.PlanningReady) + { + failureReason = string.IsNullOrEmpty(map.PlanningBlockReason) ? "规划地图尚未就绪。" : map.PlanningBlockReason; + return PlanningStatus.MapNotReady; + } + + VehicleParameters vehicle = request.Vehicle; + if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || + !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + { + failureReason = "车辆尺寸、安全余量或曲率限制无效。"; + return PlanningStatus.InvalidVehicleParameters; + } + + HybridAStarConfiguration configuration = request.Configuration; + if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || + (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) + { + failureReason = "Hybrid A* 曲率、离散、代价或资源配置无效。"; + return PlanningStatus.InvalidCurvatureConfiguration; + } + + return PlanningStatus.Success; + } + + private static bool IsValidConfiguration(HybridAStarConfiguration configuration) + { + return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && + NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && + NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && + NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && + configuration.HeadingResolutionRadians <= 2d * Math.PI && configuration.CurvatureLevelCount >= 3 && + configuration.CurvatureLevelCount % 2 == 1 && NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && + configuration.GoalPositionToleranceMeters >= 0d && NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && + configuration.GoalHeadingToleranceRadians >= 0d && configuration.MaximumExpandedNodes >= 0 && + configuration.SearchTimeout >= TimeSpan.Zero && NumericGuard.IsFinite(configuration.HeuristicWeight) && + configuration.HeuristicWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && + NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && + NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && + NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && + configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && NumericGuard.IsFinite(configuration.ClearanceCostWeight) && + configuration.ClearanceCostWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); + } + + private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) + { + if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; + + double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; + double longitudinalX = Math.Cos(pose.Heading); + double longitudinalY = Math.Sin(pose.Heading); + double lateralX = -longitudinalY; + double lateralY = longitudinalX; + for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) + for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) + { + double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; + double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + + return true; + } + + private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, + HybridAStarConfiguration configuration) + { + string reason = string.IsNullOrEmpty(searchResult.TerminationReason) + ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" + : searchResult.TerminationReason; + string resourceLimit = string.Empty; + if (searchResult.Status == PlanningStatus.SearchTimeout) + { + resourceLimit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( + "F3", CultureInfo.InvariantCulture) + "s;"; + } + else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) + { + resourceLimit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( + CultureInfo.InvariantCulture) + ";"; + } + + return reason + resourceLimit + + "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + + "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; + } + + private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, + HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) + { + TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; + return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, + reason, pathSearchElapsed)); + } + + private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, + double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) + { + return new PlanningDiagnostics( + searchResult == null ? 0 : searchResult.ExpandedNodeCount, + searchResult == null ? 0 : searchResult.GeneratedNodeCount, + searchResult == null ? 0 : searchResult.ReopenedNodeCount, + searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, + searchResult == null ? 0 : searchResult.PeakOpenListCount, + pathLengthMeters, + minimumClearanceMeters, + elapsed, + reason, + pathSearchElapsed); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) + { + if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; + if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs new file mode 100644 index 0000000..e9a12a3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; + +/// +/// 对准备发布的粗路径执行独立的连续安全与输出契约复核。 +/// 复核失败的路径不得被包装为 。 +/// +public sealed class CoarsePathValidator +{ + private const double NumericTolerance = 1e-6d; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认连续车体检查器的最终路径复核器。 + public CoarsePathValidator() + : this(new FootprintCollisionChecker()) + { + } + + /// 创建使用指定连续车体检查器的最终路径复核器。 + public CoarsePathValidator(FootprintCollisionChecker collisionChecker) + { + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 复核路径数值、曲率、连续扫掠碰撞、终点约束、累计弧长和方向分段。 + /// 参数:path 与 segments 为待发布输出;request 必须是生成该路径的请求;minimumBodyClearanceMeters 返回沿途的保守净空下界。 + /// 返回:所有规则通过时为 true;否则返回 false、写入失败原因,调用方必须丢弃 path 和 segments。 + /// + public bool TryValidate(IReadOnlyList path, IReadOnlyList segments, + PlanningRequest request, out double minimumBodyClearanceMeters, out string failureReason) + { + minimumBodyClearanceMeters = 0d; + failureReason = string.Empty; + if (path == null || segments == null || request == null || request.Map == null || request.Vehicle == null || + request.Configuration == null || path.Count == 0 || segments.Count == 0) + { + failureReason = "最终路径或复核请求为空。"; + return false; + } + + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvaturePerMeter)) + { + failureReason = "车辆曲率约束无效。"; + return false; + } + + CoarsePathPoint first = path[0]; + if (!IsValidPoint(first) || first.Source != CoarsePathPointSource.Start || first.IsGearSwitchPoint || + Math.Abs(first.ArcLength) > NumericTolerance || !IsSamePose(first, request.Start)) + { + failureReason = "最终路径首点不符合起点契约。"; + return false; + } + + double minimumClearance = double.PositiveInfinity; + for (int index = 0; index < path.Count; index++) + { + CoarsePathPoint current = path[index]; + if (!IsValidPoint(current) || Math.Abs(current.VehicleCurvature) > maximumCurvaturePerMeter + NumericTolerance) + { + failureReason = "最终路径包含非法数值或超限曲率。"; + return false; + } + + var currentPose = new Pose2D(current.X, current.Y, current.Heading); + if (!_collisionChecker.IsPoseCollisionFree(currentPose, request.Map, request.Vehicle, 0d, out double poseClearanceMeters) || + IsClearanceOverclaimed(current.BodyClearance, poseClearanceMeters)) + { + failureReason = "最终路径点未通过连续车体碰撞复核。"; + return false; + } + + minimumClearance = Math.Min(minimumClearance, current.BodyClearance); + if (index == 0) continue; + + CoarsePathPoint previous = path[index - 1]; + if (current.ArcLength + NumericTolerance < previous.ArcLength || + !IsUnwrappedHeadingContinuous(previous, current)) + { + failureReason = "最终路径的弧长或展开航向不连续。"; + return false; + } + + bool isDuplicate = IsDuplicatePoseAndArcLength(previous, current); + if (isDuplicate) + { + if (previous.Direction == current.Direction || !current.IsGearSwitchPoint) + { + failureReason = "相邻重复点不是合法换向对。"; + return false; + } + } + else + { + if (current.IsGearSwitchPoint || current.ArcLength <= previous.ArcLength + NumericTolerance || + !IsArcIncrementConsistent(previous, current)) + { + failureReason = "非换向路径点的弧长增量不一致。"; + return false; + } + } + + var previousPose = new Pose2D(previous.X, previous.Y, previous.Heading); + if (!_collisionChecker.IsSweptMotionCollisionFree(previousPose, currentPose, request.Map, request.Vehicle, + request.Configuration.MaximumCollisionCheckStepMeters, out double sweptClearanceMeters)) + { + failureReason = "最终路径相邻点之间的车体扫掠碰撞复核失败。"; + return false; + } + + minimumClearance = Math.Min(minimumClearance, sweptClearanceMeters); + } + + CoarsePathPoint last = path[path.Count - 1]; + if (!GoalToleranceChecker.IsSatisfied(new Pose2D(last.X, last.Y, last.Heading), request.Goal, request.Configuration, + last.Direction, request.GoalDirection) || (path.Count > 1 && last.Source != CoarsePathPointSource.GoalTruncation)) + { + failureReason = "最终路径末点未满足终点容差、方向或来源契约。"; + return false; + } + + if (!AreSegmentsValid(path, segments, out failureReason)) return false; + minimumBodyClearanceMeters = minimumClearance; + return true; + } + + private static bool AreSegmentsValid(IReadOnlyList path, IReadOnlyList segments, out string failureReason) + { + failureReason = string.Empty; + int expectedStartIndex = 0; + for (int segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++) + { + PathSegment segment = segments[segmentIndex]; + if (segment == null || segment.SegmentIndex != segmentIndex || segment.StartIndex != expectedStartIndex || + segment.StartIndex < 0 || segment.EndIndex < segment.StartIndex || segment.EndIndex >= path.Count || + segment.StartsAtGearSwitch != path[segment.StartIndex].IsGearSwitchPoint) + { + failureReason = "方向分段索引或起始换向标记无效。"; + return false; + } + + for (int pointIndex = segment.StartIndex; pointIndex <= segment.EndIndex; pointIndex++) + { + if (path[pointIndex].Direction != segment.Direction) + { + failureReason = "方向分段包含不同方向的路径点。"; + return false; + } + } + + bool hasNextSegment = segmentIndex + 1 < segments.Count; + bool expectedEndsAtGearSwitch = hasNextSegment && segment.EndIndex + 1 < path.Count && + path[segment.EndIndex + 1].IsGearSwitchPoint; + if (segment.EndsAtGearSwitch != expectedEndsAtGearSwitch) + { + failureReason = "方向分段末尾换向标记无效。"; + return false; + } + + expectedStartIndex = segment.EndIndex + 1; + } + + if (expectedStartIndex != path.Count) + { + failureReason = "方向分段未完整覆盖路径点。"; + return false; + } + + return true; + } + + private static bool IsValidPoint(CoarsePathPoint point) + { + if (point == null || !NumericGuard.IsFinite(point.X) || !NumericGuard.IsFinite(point.Y) || + !NumericGuard.IsFinite(point.Heading) || !NumericGuard.IsFinite(point.UnwrappedHeading) || + !NumericGuard.IsFinite(point.ArcLength) || point.ArcLength < 0d || + !NumericGuard.IsFinite(point.VehicleCurvature) || !IsTravelDirection(point.Direction) || + !IsValidClearance(point.BodyClearance)) + return false; + + return Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, AngleMath.NormalizeRadians(point.Heading))) <= NumericTolerance; + } + + private static bool IsSamePose(CoarsePathPoint point, Pose2D pose) + { + return point != null && pose != null && Math.Abs(point.X - pose.X) <= NumericTolerance && + Math.Abs(point.Y - pose.Y) <= NumericTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, pose.Heading)) <= NumericTolerance; + } + + private static bool IsUnwrappedHeadingContinuous(CoarsePathPoint previous, CoarsePathPoint current) + { + double expectedDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + return NumericGuard.IsFinite(expectedDelta) && + Math.Abs((current.UnwrappedHeading - previous.UnwrappedHeading) - expectedDelta) <= NumericTolerance; + } + + private static bool IsDuplicatePoseAndArcLength(CoarsePathPoint previous, CoarsePathPoint current) + { + return Math.Abs(previous.X - current.X) <= NumericTolerance && Math.Abs(previous.Y - current.Y) <= NumericTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(previous.Heading, current.Heading)) <= NumericTolerance && + Math.Abs(previous.ArcLength - current.ArcLength) <= NumericTolerance; + } + + private static bool IsArcIncrementConsistent(CoarsePathPoint previous, CoarsePathPoint current) + { + double expectedIncrement; + if (Math.Abs(current.VehicleCurvature) < 1e-12d) + { + double deltaX = current.X - previous.X; + double deltaY = current.Y - previous.Y; + expectedIncrement = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + } + else + { + double headingDelta = AngleMath.ShortestSignedDifference(previous.Heading, current.Heading); + expectedIncrement = Math.Abs(headingDelta / current.VehicleCurvature); + } + + return NumericGuard.IsFinite(expectedIncrement) && expectedIncrement > 0d && + Math.Abs((current.ArcLength - previous.ArcLength) - expectedIncrement) <= NumericTolerance; + } + + private static bool IsClearanceOverclaimed(double reportedClearanceMeters, double checkedClearanceMeters) + { + if (double.IsPositiveInfinity(reportedClearanceMeters)) return !double.IsPositiveInfinity(checkedClearanceMeters); + return reportedClearanceMeters > checkedClearanceMeters + NumericTolerance; + } + + private static bool IsValidClearance(double clearanceMeters) + { + return !double.IsNaN(clearanceMeters) && clearanceMeters >= 0d; + } + + private static bool IsTravelDirection(TravelDirection direction) + { + return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs new file mode 100644 index 0000000..37a55e6 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; + +/// +/// 成功搜索父链重新生成后的原语序列。 +/// 起点位置使用 m/rad,起始曲率使用 1/m;原语集合按从起点到终点的顺序排列。 +/// +public sealed class BacktrackedPath +{ + internal BacktrackedPath(Pose2D start, TravelDirection startDirection, double startCurvaturePerMeter, + IReadOnlyList primitives) + { + Start = start; + StartDirection = startDirection; + StartCurvaturePerMeter = startCurvaturePerMeter; + Primitives = primitives; + } + + /// 原始请求中的起始车辆中心位姿;位置单位 m,航向单位 rad。 + public Pose2D Start { get; } + + /// 成功父链根节点记录的起步方向。 + public TravelDirection StartDirection { get; } + + /// 成功父链根节点离散后的起始曲率,单位 1/m。 + public double StartCurvaturePerMeter { get; } + + /// 按起点到终点顺序重新积分的恒曲率原语;每项都不包含自身起点。 + public IReadOnlyList Primitives { get; } +} + +/// +/// 仅依据成功节点的父索引和原语描述,确定性地重建稠密原语序列。 +/// 不复用搜索期保存的积分点,以保证输出与当前解析积分、扫掠检查规则一致。 +/// +public sealed class PathBacktracker +{ + private const double PoseComparisonTolerance = 1e-7d; + private const double LengthComparisonTolerance = 1e-7d; + private readonly MotionPrimitiveGenerator _primitiveGenerator; + + /// 创建使用默认解析积分器的回溯器。 + public PathBacktracker() + : this(new MotionPrimitiveGenerator()) + { + } + + /// 创建使用指定原语生成器的回溯器。 + public PathBacktracker(MotionPrimitiveGenerator primitiveGenerator) + { + _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); + } + + /// + /// 依据搜索成功节点重建从起点到终点的原语序列。 + /// 参数:searchResult 必须是携带成功节点索引的搜索结果;request 必须仍指向执行搜索的同一不可变地图和配置。 + /// 返回:父链无环、索引连续且每条原语按同一解析规则可重建时为 true;失败时 path 为 null 并返回可读原因。 + /// + public bool TryBacktrack(HybridAStarSearchResult searchResult, PlanningRequest request, + out BacktrackedPath path, out string failureReason) + { + path = null; + failureReason = string.Empty; + if (searchResult == null || request == null || searchResult.Status != PlanningStatus.Success || + !searchResult.SuccessNodeIndex.HasValue || searchResult.Nodes == null) + { + failureReason = "搜索结果不含可回溯的成功节点。"; + return false; + } + + int currentIndex = searchResult.SuccessNodeIndex.Value; + var reverseNodes = new List(); + var visited = new HashSet(); + while (currentIndex >= 0) + { + if (currentIndex >= searchResult.Nodes.Count || !visited.Add(currentIndex)) + { + failureReason = "搜索父链索引越界或存在环。"; + return false; + } + + HybridAStarNode current = searchResult.Nodes[currentIndex]; + if (current == null || current.NodeIndex != currentIndex || current.Pose == null) + { + failureReason = "搜索节点索引或连续位姿不一致。"; + return false; + } + + reverseNodes.Add(current); + currentIndex = current.ParentNodeIndex; + } + + if (reverseNodes.Count == 0) + { + failureReason = "搜索父链为空。"; + return false; + } + + reverseNodes.Reverse(); + HybridAStarNode root = reverseNodes[0]; + if (root.ParentNodeIndex != -1 || root.IncomingPrimitive != null || !IsFinitePose(root.Pose)) + { + failureReason = "搜索父链根节点无效。"; + return false; + } + + var primitives = new List(Math.Max(0, reverseNodes.Count - 1)); + Pose2D previousPose = root.Pose; + for (int index = 1; index < reverseNodes.Count; index++) + { + HybridAStarNode node = reverseNodes[index]; + MotionPrimitive descriptor = node.IncomingPrimitive; + if (node.ParentNodeIndex != reverseNodes[index - 1].NodeIndex || descriptor == null || + !IsFinitePose(node.Pose) || !NumericGuard.IsFinite(descriptor.CurvaturePerMeter) || + !IsTravelDirection(descriptor.Direction) || descriptor.ActualLengthMeters <= 0d) + { + failureReason = "搜索父链中的原语描述无效。"; + return false; + } + + // 只把恒曲率、方向和长度描述作为真源,再走一遍相同的解析积分与连续碰撞检查。 + MotionPrimitive rebuilt = _primitiveGenerator.Generate(previousPose, descriptor.CurvaturePerMeter, descriptor.Direction, request); + if (rebuilt == null || !IsEquivalent(descriptor, rebuilt) || !IsSamePose(rebuilt.End, node.Pose)) + { + failureReason = "搜索原语无法按当前解析规则确定性重建。"; + return false; + } + + primitives.Add(rebuilt); + previousPose = rebuilt.End; + } + + path = new BacktrackedPath(root.Pose, root.Direction, root.CurvaturePerMeter, + new ReadOnlyCollection(primitives)); + return true; + } + + private static bool IsEquivalent(MotionPrimitive expected, MotionPrimitive actual) + { + return expected.Direction == actual.Direction && expected.IsGoalTruncation == actual.IsGoalTruncation && + Math.Abs(expected.CurvaturePerMeter - actual.CurvaturePerMeter) <= PoseComparisonTolerance && + Math.Abs(expected.ActualLengthMeters - actual.ActualLengthMeters) <= LengthComparisonTolerance; + } + + private static bool IsSamePose(Pose2D first, Pose2D second) + { + return IsFinitePose(first) && IsFinitePose(second) && + Math.Abs(first.X - second.X) <= PoseComparisonTolerance && + Math.Abs(first.Y - second.Y) <= PoseComparisonTolerance && + Math.Abs(AngleMath.ShortestSignedDifference(first.Heading, second.Heading)) <= PoseComparisonTolerance; + } + + 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; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md new file mode 100644 index 0000000..730a335 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md @@ -0,0 +1,387 @@ +# CoarsePath 粗路径规划(P0/P1) + +`CoarsePath` 在 `Map` 提供的不可变 `PlanningGridMap` 上执行 Hybrid A*,输出已经过连续碰撞、终点和输出不变量复核的粗路径。它只处理“能否安全地从当前车辆几何中心到达目标”的几何搜索;不读取传感器或定位,不绘制 UI,也不向底盘发送任何命令。 + +地图障碍物来源、世界坐标栅格化、距离场和缓存细节由 [Map/README.md](../Map/README.md) 说明。本模块唯一建议的业务调用入口是: + +```csharp +CoarsePathPlanningService.Plan(job, cancellationToken) +``` + +## 模块说明(Module Overview) + +| 模块 | 负责内容 | 不负责内容 | +| --- | --- | --- | +| `Map` | 外部障碍物快照、占据栅格、保守障碍距离、地图缓存 | 车辆足迹、运动原语、路径搜索、控制 | +| `CoarsePath` | 车辆扩大足迹、连续碰撞检查、前进/倒车原语、Hybrid A*、路径复核与方向分段 | 传感器读取、定位读取、速度规划、路径跟踪、底盘命令 | +| `Facade` | 将建图、搜索、取消、总预算和可选调试旁路编排为一次调用 | 修改地图内容、写入 AMR 占据、执行轨迹 | +| `Test` | 固定回归场景、P1 手动测试入口与规划结果可视化 | 真实作业地图、实时重规划或车辆控制 | + +安全余量只由 `VehicleParameters.SafetyMarginMeters` 扩大车辆矩形。它不会回写到地图障碍物,因此同一个 `PlanningGridMap` 可由不同车辆参数重复使用。 + +## 文件结构(File Structure) + +```text +CoarsePath/ +├── README.md # 本模块说明:结构、数据流、调用与测试 +├── HybridAStarPlanner.cs # 公开规划门面后的核心编排:校验、搜索、回溯和复核 +├── Contracts/ +│ ├── Pose2D.cs # 车辆几何中心位姿:m / rad +│ ├── PlanningRequest.cs # 已有 PlanningGridMap 上的一次内部搜索请求 +│ ├── PlanningResult.cs # 不可变规划结果:成功路径或空结果 +│ ├── PlanningStatus.cs # 成功、取消、无解、输入和资源限制状态 +│ ├── CoarsePathPoint.cs # 稠密路径点、方向、曲率、净空和换向标记 +│ ├── PathSegment.cs # 前进或倒车的包含式路径索引段 +│ ├── VehicleParameters.cs # 车体尺寸、安全余量和曲率限制 +│ ├── HybridAStarConfiguration.cs # 原语、离散、代价、容差和资源上限 +│ └── GoalDirectionConstraint.cs # 目标进入方向约束 +├── Vehicle/ +│ ├── VehicleKinematics.cs # 恒曲率车辆运动学积分 +│ ├── VehicleFootprint.cs # 扩大后的车辆矩形几何 +│ ├── OrientedRectangleCellIntersection.cs # 旋转矩形与占据格相交判定 +│ └── FootprintCollisionChecker.cs # 连续扫掠的足迹碰撞检查 +├── Search/ +│ ├── BinaryMinHeap.cs # 可更新优先级的 Open List +│ ├── GridDijkstraHeuristic.cs # 二维栅格可达性和距离启发式 +│ ├── GoalToleranceChecker.cs # 目标位置、航向和方向约束判定 +│ ├── MotionPrimitive.cs # 单个前进或倒车恒曲率原语 +│ ├── MotionPrimitiveGenerator.cs # 原语离散与连续积分点生成 +│ ├── SearchCostCalculator.cs # 长度、倒车、换向、曲率和净空代价 +│ ├── HybridAStarNode.cs # 搜索节点与父链信息 +│ ├── HybridAStarNodeKey.cs # 离散状态键 +│ └── HybridAStarSearch.cs # Hybrid A* 主搜索循环 +├── Output/ +│ ├── PathBacktracker.cs # 从终点节点安全回溯父链 +│ ├── CoarsePathAssembler.cs # 组装稠密路径和方向段 +│ └── CoarsePathValidator.cs # 对最终输出重新进行连续复核 +├── Facade/ +│ ├── CoarsePathPlanningJob.cs # 一次完整业务输入:地图请求、位姿、车辆、配置 +│ ├── CoarsePathPlanningJobResult.cs # 同时包含地图结果和规划结果的不可变输出 +│ ├── CoarsePathPlanningService.cs # 唯一业务调用门面 +│ ├── PlanningDebugOptions.cs # 可选调试旁路配置 +│ └── IPlanningDebugSink.cs # 调试旁路接收器契约 +└── Test/ + ├── CoarsePathScenarioFactory.cs # 六个固定场景和手动目标演示请求工厂 + └── MovementTest.CoarsePathTest.cs # 七个 Clumsy 入口、后台取消和 Painter 绘制 +``` + +## 规划数据流(Planning Data Flow) + +```text +CoarsePathPlanningJob + │ MapRequest 使用 mm;Pose2D/车辆使用 m、rad + ▼ +CoarsePathPlanningService.Plan(job, cancellationToken) + │ + ├── PlanningMapFactory.Create(job.MapRequest) + │ │ + │ ├── 失败、取消或超时 + │ │ └── MapResult + 空路径 PlanningResult,搜索不启动 + │ │ + │ └── 成功:不可变 PlanningGridMap + ▼ +PlanningRequest + │ + ▼ +HybridAStarPlanner + ├── 车辆扩大足迹与连续碰撞检查 + ├── GridDijkstraHeuristic + Hybrid A* 搜索 + ├── PathBacktracker + CoarsePathAssembler + └── CoarsePathValidator 最终复核 + ▼ +PlanningResult + MapResult + ▼ +CoarsePathPlanningJobResult +``` + +调用方只创建 `CoarsePathPlanningJob` 并消费 `CoarsePathPlanningJobResult`。`PlanningRequest`、`HybridAStarPlanner`、原语和碰撞检查器属于模块内部协作对象,不应由 UI、传感器或 MovementTest 直接拼接。 + +## 构建状态与停止(Build Status and Stop) + +必须一起处理 `MapResult` 和 `PlanningResult`。`MapResult.Status` 的类型是 `PlanningMapBuildStatus`;地图失败时,门面返回对应的空路径结果,并且不会启动 Hybrid A*。 + +| 情况 | `MapResult` | `PlanningResult` | 调用方处理 | +| --- | --- | --- | --- | +| 地图和搜索成功 | `Success` 且 `Map` 非空 | `Success`,发布完整路径和方向段 | 消费粗路径;后续模块仍需自行进行平滑、速度和控制 | +| 地图输入/来源失败 | `Failed` | `InvalidMap` | 读取 `FailureReason`,修复地图输入 | +| 调用被取消 | `Cancelled` | `Cancelled` | 不重试为普通无解;不会发布地图或部分路径 | +| 总预算耗尽 | `TimedOut` | `SearchTimeout` | 根据上层策略调整预算或稍后重试 | +| 搜索无解 | 地图成功 | `NoFeasiblePath` | 当前地图、车体和运动约束下无可行路径 | +| 节点或搜索资源受限 | 地图成功 | `SearchNodeLimitExceeded` 或 `SearchTimeout` | 读取诊断后调整配置或上层策略 | + +除 `PlanningStatus.Success` 外,`PlanningResult.Path` 与 `PlanningResult.Segments` 始终为空。取消、超时、无解、输入错误和最终复核失败都不能作为“部分可执行路径”使用。 + +### 总预算与取消 + +`HybridAStarConfiguration.SearchTimeout` 是从门面开始计时的一次总预算,依次覆盖建图、距离场、二维启发式和 Hybrid A*。同一个 `CancellationToken` 会沿调用链传递,取消优先于超时。 + +### 总耗时与路径搜索耗时 + +`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed`(路径搜索耗时)从地图和起终点预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核;搜索开始前失败时为零。 + +## 坐标与单位(Coordinates and Units) + +| 数据 | 单位 | 说明 | +| --- | --- | --- | +| `PlanningMapRequest.Bounds`、分辨率、障碍物几何 | mm | 来自 Map 的世界坐标;边界采用 `[min, max)` | +| `Pose2D.X`、`Pose2D.Y`、路径位置、车辆尺寸、安全余量、弧长 | m | CoarsePath 的连续世界坐标和长度 | +| `Pose2D.Heading`、航向容差 | rad | 核心一律使用弧度 | +| 曲率、起步曲率 | 1/m | 最大曲率或最小转弯半径至少提供一个 | +| `PlanningGridMap` 世界查询参数 | m | 越界位置按占据处理,净距为 0 | +| P1 的 AMR/手动目标 X/Y | mm | 仅在 UI 边界读取,进入核心前除以 1000 | +| P1 的 AMR/手动目标航向 | deg | 仅在 UI 边界转换为 `deg * PI / 180 -> rad` | + +起点和终点都表示车辆**几何中心**。若上游定位参考点是雷达、天线或其他安装点,必须先在上游应用安装外参;不要在 CoarsePath 内猜测偏移。车辆外扩由 `VehicleParameters.SafetyMarginMeters` 表达,不要把余量写入 Map 障碍物。 + +## 最小调用示例(Minimal Call Example) + +以下示例明确允许空图,因而只适合算法或单位演示。真实作业必须通过 `IMapObstacleSource` 提供有效障碍物快照;如何构造来源请阅读 [Map/README.md](../Map/README.md)。 + +```csharp +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.Mapping; + +var service = new CoarsePathPlanningService(); // 长期持有,保留地图缓存 + +var job = new CoarsePathPlanningJob +{ + MapRequest = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = Array.Empty(), + AllowExplicitEmptyMap = true, // 仅演示时明确允许 + }, + Start = new Pose2D(1d, 1d, 0d), + Goal = new Pose2D(3d, 1d, 0d), + Vehicle = new VehicleParameters + { + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, + }, + Configuration = new HybridAStarConfiguration(), + GoalDirection = GoalDirectionConstraint.Forward, +}; + +CoarsePathPlanningJobResult result = + service.Plan(job, CancellationToken.None); + +if (!result.MapResult.Succeeded) + throw new InvalidOperationException(result.MapResult.FailureReason); + +if (result.PlanningResult.Status != PlanningStatus.Success) + throw new InvalidOperationException( + result.PlanningResult.Diagnostics.TerminationReason); + +foreach (CoarsePathPoint point in result.PlanningResult.Path) + Console.WriteLine(point.X + "," + point.Y + "," + point.Heading); +``` + +## 缓存与 SourceVersion(Cache and SourceVersion) + +`CoarsePathPlanningService` 在生命周期内长期持有 `PlanningMapFactory`,因此重复调用时能够复用地图缓存。不要每次规划都新建服务,否则会失去缓存收益。 + +| 缓存层级 | 条件 | 结果 | +| --- | --- | --- | +| `Input` | 边界、分辨率、空图策略、来源 ID、`SourceVersion`、必需性和来源结果相同 | 返回同一个不可变 `PlanningGridMap` | +| `Occupancy` | 输入版本变化,但最终占据栅格相同 | 复用占据/距离数组,生成新的快照元数据 | +| `None` | 占据内容变化 | 重建规划快照和距离场 | + +障碍来源内容改变时,调用方必须递增该来源的 `SourceVersion`。仅修改起终点、车辆、搜索配置、调试开关或调试接收器不会改变地图输入;改变障碍物却不递增版本则可能错误复用旧快照。 + +## 详细使用指南(Detailed Usage Guide) + +本节说明调用方如何从一个地图输入得到可消费的粗路径。所有业务调用都通过 `CoarsePathPlanningService.Plan(job, cancellationToken)` 完成。 + +### 第 1 步:长期持有服务 + +服务持有地图工厂和规划器,应该作为规划业务、任务执行器或上层服务的长期字段,而不是在每次调用中创建: + +```csharp +private readonly CoarsePathPlanningService _coarsePathService = + new CoarsePathPlanningService(); +``` + +### 第 2 步:准备地图请求 + +创建 `PlanningMapRequest`,其边界、分辨率和障碍物仍使用 mm。使用手工圆形/矩形或 TwoLeg 快照时,应先按 [Map/README.md](../Map/README.md) 将它们包装为 `IMapObstacleSource`,并为内容变化递增 `SourceVersion`。 + +```csharp +var mapRequest = new PlanningMapRequest +{ + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = sources, + AllowExplicitEmptyMap = false, +}; +``` + +`AllowExplicitEmptyMap = true` 只在调用方明确确认空地图安全时使用。未提供有效障碍物且未显式允许空图时,地图不会进入规划。 + +### 第 3 步:填写起点、终点和方向约束 + +将车辆几何中心的世界 X/Y 从 mm 转为 m,并将航向转换为 rad 后创建 `Pose2D`。`StartDirection = null` 表示允许从前进或倒车开始;`GoalDirection` 可以限制最终进入目标的方向。 + +```csharp +var start = new Pose2D(startXmm / 1000d, startYmm / 1000d, + startHeadingDeg * Math.PI / 180d); +var goal = new Pose2D(goalXmm / 1000d, goalYmm / 1000d, + goalHeadingDeg * Math.PI / 180d); +``` + +### 第 4 步:填写车辆参数 + +车辆尺寸和安全余量全部为 m。曲率限制可填写 `MaximumCurvaturePerMeter`,或填写 `MinimumTurningRadiusMeters`;至少必须提供一个有效限制。 + +```csharp +var vehicle = new VehicleParameters +{ + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, +}; +``` + +### 第 5 步:调整搜索配置 + +默认 `HybridAStarConfiguration` 包含原语长度、积分步长、航向离散、终点容差、代价、节点上限和总超时。若业务需要覆盖默认值,应同时理解安全影响:`MaximumCollisionCheckStepMeters` 不能以牺牲连续碰撞检查精度为代价随意增大。 + +```csharp +var configuration = new HybridAStarConfiguration +{ + SearchTimeout = TimeSpan.FromSeconds(5d), + MaximumExpandedNodes = 200000, +}; +``` + +### 第 6 步:调用并消费成功结果 + +只有 `Success` 可以发布完整路径。`Path` 是稠密点序列;`Segments` 是覆盖整条路径的前进/倒车包含式索引段,可供后续的速度规划或显示模块消费。 + +```csharp +var job = new CoarsePathPlanningJob +{ + MapRequest = mapRequest, + Start = start, + Goal = goal, + Vehicle = vehicle, + Configuration = configuration, + GoalDirection = GoalDirectionConstraint.Any, +}; + +CoarsePathPlanningJobResult result = _coarsePathService.Plan(job, cancellationToken); +if (!result.MapResult.Succeeded) + ReportMapFailure(result.MapResult.FailureReason); +else if (result.PlanningResult.Status == PlanningStatus.Success) + ConsumeCoarsePath(result.PlanningResult.Path, result.PlanningResult.Segments); +else + ReportPlanningFailure(result.PlanningResult.Diagnostics.TerminationReason); +``` + +`CoarsePathPoint.IsGearSwitchPoint` 为 `true` 表示该点是新方向段开始处。换向位置会保留一对位置、航向与弧长相同、方向不同的相邻点;`UnwrappedHeading` 用于跨越 `-pi/pi` 时保持显示连续。 + +## P1 手动测试与可视化(P1 Manual Tests and Visualization) + +P1 在 `Test/MovementTest.CoarsePathTest.cs` 提供只读测试入口。它们共用一个长期存活的 `CoarsePathPlanningService`,只提交规划并绘制结果;不发送底盘、速度或转向命令。 + +| MovementTest 名称 | 场景 | 预期 | +| --- | --- | --- | +| `粗路径规划-显式空图` | 显式允许的空图 | 前进直达成功 | +| `粗路径规划-单矩形绕行` | 中央矩形阻断直线 | 成功绕障 | +| `粗路径规划-多来源障碍` | 手工圆形、矩形和 TwoLeg 快照 | 证明多来源经过同一门面 | +| `粗路径规划-缓存命中` | 重复相同地图输入 | 后续调用显示 `Input` 缓存命中 | +| `粗路径规划-倒车换向` | 前进起步、倒车到达 | 成功路径含 `IsGearSwitchPoint` | +| `粗路径规划-无解` | 贯穿地图的障碍带 | 返回 `NoFeasiblePath` 且不发布路径 | +| `粗路径规划` | 当前 AMR 位姿、人工终点和可选人工障碍物 | 验证手动障碍物、边界、路径与可视化 | + +### 固定案例的实时 AMR 锚定 + +`粗路径规划-显式空图`、`粗路径规划-单矩形绕行`、`粗路径规划-多来源障碍`、`粗路径规划-缓存命中`、`粗路径规划-倒车换向` 和 `粗路径规划-无解` 是六个固定案例。它们不再以写死的世界起点运行:共享运行器在前台仅调用一次 `DetourInterface.getCartLocation()`,校验并冻结本次 AMR 的世界 `X(mm)`、`Y(mm)` 与航向 `deg`,再创建本次规划请求;后台规划期间不会再次读取定位。 + +`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)` + +该入口把基准案例的起点映射为冻结的 AMR 位姿,并以相同的 `ΔX/ΔY` 平移地图边界、目标、圆形/矩形障碍,以及 TwoLeg 的检测原点。因此,固定案例始终在当前 AMR 附近保留原有的相对几何关系。起点航向严格使用冻结的 AMR 航向;终点航向保持基准案例的“终点航向减起点航向”差值,叠加到当前 AMR 航向后规范化到 `[-pi, pi]`。TwoLeg 只平移检测原点,`DetectionHeadingRadians` 不会因 AMR 航向发生旋转。 + +`粗路径规划-缓存命中` 只有两次运行冻结到相同的 AMR `X/Y`、从而形成相同的平移后地图输入时,才作为缓存命中场景;AMR 位置移动后,地图输入正常未命中并重建快照。仅 AMR 航向变化不会改变固定案例的地图输入,地图缓存仍可命中。若定位读取为空、抛出异常,或 `X`、`Y`、航向含有 `NaN`/无穷值,运行器不会提交后台规划,状态与 Toast 会显示以“AMR 位姿不可用”开头的诊断原因。 + +手动 `粗路径规划` 的人工终点、障碍物和超时输入流程保持不变;它不套用固定案例的整体平移规则。 + +### AMR 位姿、手动终点与障碍物 + +`CoarsePathPlanningTest` 启动时读取一次 `DetourInterface.getCartLocation()`,将当前 AMR 世界位姿冻结为起点;随后依次输入目标世界 `X(mm)`、`Y(mm)`、航向 `deg`,以及障碍物数量 `0-20`。每个障碍物再依次输入类型和几何参数: + +| 类型输入 | 形状 | 输入参数(全部为 mm) | +| --- | --- | --- | +| `1` | 圆形 | 几何中心 `X`、`Y` 与半径;半径必须大于 0 | +| `2` | 轴对齐矩形 | 几何中心 `X`、`Y`、X 向长度、Y 向宽度;两个尺寸必须大于 0 | + +矩形只支持 `AxisAlignedRectangle`,不提供旋转角;其中心和长宽由 `ManualCoarsePathObstacle.AxisAlignedRectangle` 表达。圆形由 `ManualCoarsePathObstacle.Circle` 表达。所有无穷、NaN、非数字或不合法尺寸都会在输入阶段拒绝。 + +`CoarsePathScenarioFactory.CreateManualObstacleDemo` 在唯一边界完成转换: + +```text +AMR/目标 X、Y:mm / 1000 -> m +AMR/目标航向:deg * PI / 180 -> rad +``` + +当数量为 0 时,入口使用显式空图;`CreateManualGoalDemo` 保留为同一零障碍物场景的兼容帮助方法。数量大于 0 时,工厂将 `ManualCoarsePathObstacle` 快照封装成来源 ID 为 `manual-user-input` 的地图输入,并为每次手动快照分配新的 `SourceVersion`,避免错误复用地图缓存。规划边界覆盖起点、终点和每个障碍物的完整轮廓,再增加 8000 mm 余量并按 50 mm 对齐。 + +手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。 + +此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。 + +`getCartLocation` 在无有效定位时可能阻塞,因此应在定位准备完成的测试环境使用。手动障碍物是测试输入,不能替代现场障碍物来源;零障碍物的显式空图也绝不代表现场不存在障碍物。 + +### 后台执行、停止与图层 + +每次测试启动时会创建独立的 `CancellationTokenSource`,以 `Task.Run` 调用门面,并先取消旧会话。`Test()` 不等待任务,也不读取 `Task.Result`;`TestStop` 取消当前令牌、使会话失效并清空图层。已取消任务完成后不会覆盖新会话,也不会显示部分路径。 + +专用世界坐标 Painter 图层为 `CoarsePathPlanningV1`。它直接读取本次 `PlanningGridMap` 的 `Bounds`、`ResolutionMm`、`SnapshotId` 和 `IsOccupied(row, col)`,因此边界、抽稀网格和占据格与实际规划快照一致,而不是重新绘制原始障碍物。 + +状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加 `TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。 + +| 颜色 | 可视化元素 | +| --- | --- | +| 灰白 | 地图边界与栅格网络 | +| 暗红 | 占据格 | +| 绿色 | 起点、前进路径和方向箭头 | +| 橙色 | 终点、航向和位置容差圈 | +| 天蓝 | 倒车路径和方向箭头 | +| 紫色 | 换向点 | +| 金色 | 已纳入安全余量的车辆检查框 | + +自动化已检查 UI 入口的后台、取消和数据来源结构。仍需在实际 Clumsy 界面手动运行“粗路径规划-单矩形绕行”和“粗路径规划”,确认图层交互显示与停止按钮效果。 + +## 常见错误(Common Errors) + +| 现象 | 原因 | 处理 | +| --- | --- | --- | +| 起点、终点或障碍物位置相差 1000 倍 | 将 mm 直接传给 `Pose2D` 或把 m 传给地图输入 | Map 输入使用 mm;`Pose2D`、车辆和路径使用 m | +| 路径朝向错误或旋转异常 | 将 P1 的 deg 直接当作核心 rad | 在 UI/上层边界执行 `deg * PI / 180`,核心只保存 rad | +| 障碍物已经变化却复用旧地图 | 内容变更后没有递增 `SourceVersion` | 每次来源快照内容变化后增加对应版本号 | +| 地图创建成功但规划被阻止 | 没有有效障碍物且未显式允许空图 | 提供有效来源;仅在确认安全的演示中设置 `AllowExplicitEmptyMap = true` | +| 无解、取消或超时后仍尝试使用路径 | 没有检查 `PlanningStatus.Success` | 仅成功时消费 `Path` 和 `Segments`;其他状态读取诊断 | +| 将粗路径直接下发给车辆 | 粗路径不包含速度、时间、执行控制或实时安全闭环 | 在后续阶段增加平滑、时间参数化、跟踪和独立安全控制 | +| P1 手动终点表现为空场地安全 | 手动入口使用显式空图演示 | 真实作业必须提供真实障碍物快照,不能复用空图语义 | + +## 第一版限制(First-Version Limits) + +当前 P0/P1 已提供安全、确定性的粗路径核心和手动结果可视化,但不包含: + +- 路径平滑或曲率连续优化; +- Reeds-Shepp 或 Dubins 精确终点连接; +- 速度、加速度、时间标注、时间轨迹和路径跟踪控制; +- 底盘命令、避障闭环、现场传感器采集或实时重规划调度; +- 横移、蟹行或其他非前进/倒车运动原语; +- 原地旋转; +- 真实作业地图接入、交互式场景编辑和 Release 性能/资源/确定性基准。 + +当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。 + +因此,调用方只能把 `Success` 结果视作后续模块的粗路径输入,不能把它当作可直接下发的时间轨迹。 diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs new file mode 100644 index 0000000..5e2f369 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 为 Hybrid A* Open List 提供确定性优先级的二叉最小堆。 +/// 排序严格依次比较 F、H、较大的 G 和插入序号;F、H、G 必须为有限且非负的等效米代价。 +/// +/// 与一组搜索代价关联的节点或条目类型。 +public sealed class BinaryMinHeap +{ + private readonly List _entries = new List(); + private long _nextInsertionSequence; + + /// 创建空的确定性 Open List 堆。 + public BinaryMinHeap() + { + } + + /// 当前堆内尚未出队的条目数量。 + public int Count { get { return _entries.Count; } } + + /// + /// 将一个条目和其搜索排序代价压入堆。 + /// 参数:item 为关联条目;f、h、g 均为有限且非负的等效米代价。 + /// 失败:任一代价无效、item 为 null(仅引用类型)或插入序号耗尽时抛出异常。 + /// + public void Push(T item, double f, double h, double g) + { + if (ReferenceEquals(item, null)) throw new ArgumentNullException(nameof(item)); + ValidateCost(f, nameof(f)); + ValidateCost(h, nameof(h)); + ValidateCost(g, nameof(g)); + if (_nextInsertionSequence == long.MaxValue) + throw new InvalidOperationException("The binary heap insertion sequence has been exhausted."); + + var entry = new HeapEntry(item, f, h, g, _nextInsertionSequence); + _nextInsertionSequence++; + _entries.Add(entry); + SiftUp(_entries.Count - 1); + } + + /// + /// 弹出当前排序最优的条目。 + /// 返回:按 F、H、较大 G 和插入序号排序后的最小条目;空堆时抛出 。 + /// + public T Pop() + { + if (_entries.Count == 0) throw new InvalidOperationException("The binary heap is empty."); + + HeapEntry result = _entries[0]; + int lastIndex = _entries.Count - 1; + if (lastIndex == 0) + { + _entries.RemoveAt(0); + return result.Item; + } + + _entries[0] = _entries[lastIndex]; + _entries.RemoveAt(lastIndex); + SiftDown(0); + return result.Item; + } + + /// 清空尚未出队的条目;后续插入序号继续单调递增以保持整个实例内的确定性。 + public void Clear() + { + _entries.Clear(); + } + + private void SiftUp(int index) + { + while (index > 0) + { + int parentIndex = (index - 1) / 2; + if (Compare(_entries[index], _entries[parentIndex]) >= 0) return; + Swap(index, parentIndex); + index = parentIndex; + } + } + + private void SiftDown(int index) + { + while (true) + { + int leftChildIndex = index * 2 + 1; + if (leftChildIndex >= _entries.Count) return; + + int bestChildIndex = leftChildIndex; + int rightChildIndex = leftChildIndex + 1; + if (rightChildIndex < _entries.Count && Compare(_entries[rightChildIndex], _entries[leftChildIndex]) < 0) + bestChildIndex = rightChildIndex; + + if (Compare(_entries[bestChildIndex], _entries[index]) >= 0) return; + Swap(index, bestChildIndex); + index = bestChildIndex; + } + } + + private void Swap(int firstIndex, int secondIndex) + { + HeapEntry temporary = _entries[firstIndex]; + _entries[firstIndex] = _entries[secondIndex]; + _entries[secondIndex] = temporary; + } + + private static int Compare(HeapEntry left, HeapEntry right) + { + int comparison = left.F.CompareTo(right.F); + if (comparison != 0) return comparison; + + comparison = left.H.CompareTo(right.H); + if (comparison != 0) return comparison; + + comparison = right.G.CompareTo(left.G); + if (comparison != 0) return comparison; + + return left.InsertionSequence.CompareTo(right.InsertionSequence); + } + + private static void ValidateCost(double value, string parameterName) + { + if (!NumericGuard.IsFinite(value) || value < 0d) + throw new ArgumentOutOfRangeException(parameterName, "Search costs must be finite and non-negative."); + } + + private sealed class HeapEntry + { + public HeapEntry(T item, double f, double h, double g, long insertionSequence) + { + Item = item; + F = f; + H = h; + G = g; + InsertionSequence = insertionSequence; + } + + public T Item { get; } + public double F { get; } + public double H { get; } + public double G { get; } + public long InsertionSequence { get; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs new file mode 100644 index 0000000..3d18a04 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs @@ -0,0 +1,61 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// 按位置、航向和进入方向约束判定连续位姿是否可以作为目标候选。 +public static class GoalToleranceChecker +{ + /// + /// 判断一个位姿是否满足目标容差和进入方向约束。 + /// 参数:pose 与 goal 使用世界 m/rad;configuration 提供位置 m 和航向 rad 容差;direction 为候选末段方向;goalDirection 为目标进入方向约束。 + /// 返回:输入有限、位置距离和最小环形航向误差均不超过容差且方向匹配时为 true;无效输入保守地返回 false。 + /// + public static bool IsSatisfied( + Pose2D pose, + Pose2D goal, + HybridAStarConfiguration configuration, + TravelDirection direction, + GoalDirectionConstraint goalDirection) + { + if (!IsFinitePose(pose) || !IsFinitePose(goal) || configuration == null || + !NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) || + !NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) || + configuration.GoalPositionToleranceMeters < 0d || configuration.GoalHeadingToleranceRadians < 0d || + !IsTravelDirection(direction) || !IsGoalDirection(goalDirection)) + return false; + + if (!MatchesDirection(direction, goalDirection)) return false; + + double deltaX = pose.X - goal.X; + double deltaY = pose.Y - goal.Y; + double positionDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + if (!NumericGuard.IsFinite(positionDistanceMeters) || positionDistanceMeters > configuration.GoalPositionToleranceMeters) + return false; + + double headingDifferenceRadians = Math.Abs(AngleMath.ShortestSignedDifference(pose.Heading, goal.Heading)); + return NumericGuard.IsFinite(headingDifferenceRadians) && headingDifferenceRadians <= configuration.GoalHeadingToleranceRadians; + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private static bool MatchesDirection(TravelDirection direction, GoalDirectionConstraint constraint) + { + return constraint == GoalDirectionConstraint.Any || + (constraint == GoalDirectionConstraint.Forward && direction == TravelDirection.Forward) || + (constraint == GoalDirectionConstraint.Reverse && direction == TravelDirection.Reverse); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs new file mode 100644 index 0000000..509bf8a --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 基于不可变栅格地图的目标反向八邻域 Dijkstra 启发式。 +/// 距离单位为 m;对角移动仅在两个对应正交邻格都未占据时允许,以避免从障碍夹角穿越。 +/// +public sealed class GridDijkstraHeuristic +{ + private readonly PlanningGridMap _map; + private readonly double[] _costs; + + /// + /// 从目标栅格预计算所有可达自由格到目标的二维最短距离。 + /// 参数:map 必须是已就绪的不可变地图;goalRow、goalCol 为地图内且未占据的目标格索引。 + /// 失败:地图为空、未就绪或目标格无效时抛出异常。 + /// + public GridDijkstraHeuristic(PlanningGridMap map, int goalRow, int goalCol) + { + if (!TryCreate(map, goalRow, goalCol, PlanningOperationBudget.Unlimited(CancellationToken.None), + out GridDijkstraHeuristic heuristic, out _)) + throw new InvalidOperationException("Unbounded Dijkstra construction unexpectedly stopped."); + _map = heuristic._map; + _costs = heuristic._costs; + } + + private GridDijkstraHeuristic(PlanningGridMap map, double[] costs) + { + _map = map; + _costs = costs; + } + + /// 使用共享预算创建完整二维启发式;停止时不返回部分成本数组。 + internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, PlanningOperationBudget budget, + out GridDijkstraHeuristic heuristic, out PlanningOperationStopReason stopReason) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + if (!map.PlanningReady) throw new ArgumentException("The planning map must be ready.", nameof(map)); + if (goalRow < 0 || goalRow >= map.Rows || goalCol < 0 || goalCol >= map.Cols) + throw new ArgumentOutOfRangeException(nameof(goalRow)); + if (map.IsOccupied(goalRow, goalCol)) + throw new ArgumentException("The goal grid cell must be free.", nameof(goalRow)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + + heuristic = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + var costs = new double[checked(map.Rows * map.Cols)]; + int workItemCount = 0; + for (int index = 0; index < costs.Length; index++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + costs[index] = double.PositiveInfinity; + } + if (!TryBuild(map, costs, goalRow, goalCol, budget, ref workItemCount, out stopReason)) return false; + heuristic = new GridDijkstraHeuristic(map, costs); + stopReason = PlanningOperationStopReason.None; + return true; + } + + /// + /// 查询指定栅格到构造时目标格的二维最短距离。 + /// 参数:row、col 为从零开始的栅格索引。 + /// 返回:单位 m 的有限最短距离;自由格不可达或索引越界时返回正无穷。 + /// + public double GetCost(int row, int col) + { + return row < 0 || row >= _map.Rows || col < 0 || col >= _map.Cols + ? double.PositiveInfinity + : _costs[row * _map.Cols + col]; + } + + private static bool TryBuild(PlanningGridMap map, double[] costs, int goalRow, int goalCol, + PlanningOperationBudget budget, ref int workItemCount, out PlanningOperationStopReason stopReason) + { + var openList = new BinaryMinHeap(); + int goalIndex = goalRow * map.Cols + goalCol; + costs[goalIndex] = 0d; + openList.Push(goalIndex, 0d, 0d, 0d); + + while (openList.Count > 0) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + int currentIndex = openList.Pop(); + int currentRow = currentIndex / map.Cols; + int currentCol = currentIndex % map.Cols; + double currentCost = costs[currentIndex]; + + for (int rowOffset = -1; rowOffset <= 1; rowOffset++) + for (int colOffset = -1; colOffset <= 1; colOffset++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + if (rowOffset == 0 && colOffset == 0) continue; + + int nextRow = currentRow + rowOffset; + int nextCol = currentCol + colOffset; + if (nextRow < 0 || nextRow >= map.Rows || nextCol < 0 || nextCol >= map.Cols || map.IsOccupied(nextRow, nextCol)) + continue; + + bool isDiagonal = rowOffset != 0 && colOffset != 0; + if (isDiagonal && (map.IsOccupied(currentRow + rowOffset, currentCol) || map.IsOccupied(currentRow, currentCol + colOffset))) + continue; + + double stepCost = isDiagonal ? Math.Sqrt(2d) * map.ResolutionMeters : map.ResolutionMeters; + double candidateCost = currentCost + stepCost; + int nextIndex = nextRow * map.Cols + nextCol; + if (candidateCost >= costs[nextIndex]) continue; + + costs[nextIndex] = candidateCost; + openList.Push(nextIndex, candidateCost, 0d, 0d); + } + } + stopReason = PlanningOperationStopReason.None; + return true; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs new file mode 100644 index 0000000..b3ce03d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs @@ -0,0 +1,99 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// Hybrid A* 运行期节点。 +/// 节点保留连续位姿和其离散闭集键;搜索过程中不会修改已创建节点,改进代价时会追加新节点并使旧 Open List 条目失效。 +/// +public sealed class HybridAStarNode +{ + /// + /// 创建一个搜索节点。 + /// 参数:nodeIndex 为本次搜索内稳定索引;key 为离散状态;pose 为连续车辆中心位姿;parentNodeIndex 为父节点索引,根节点使用 -1; + /// incomingPrimitive 为父节点到本节点的原语,根节点为 null;curvaturePerMeter、gCostMeters 和 hCostMeters 均使用规划器的标准单位。 + /// + public HybridAStarNode( + int nodeIndex, + HybridAStarNodeKey key, + Pose2D pose, + int parentNodeIndex, + MotionPrimitive incomingPrimitive, + double curvaturePerMeter, + double gCostMeters, + double hCostMeters) + : this(nodeIndex, key, pose, parentNodeIndex, incomingPrimitive, curvaturePerMeter, gCostMeters, hCostMeters, false) + { + } + + /// + /// 创建一个搜索节点,并显式指定它是否为原语内部命中的终点候选。 + /// 参数:isGoalCandidate 为 true 时,本节点不参与普通离散状态的最优 G 值支配;它仍必须在从 Open List 出队后重新通过连续终点和碰撞检查才能成功。 + /// + public HybridAStarNode( + int nodeIndex, + HybridAStarNodeKey key, + Pose2D pose, + int parentNodeIndex, + MotionPrimitive incomingPrimitive, + double curvaturePerMeter, + double gCostMeters, + double hCostMeters, + bool isGoalCandidate) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (pose == null) throw new ArgumentNullException(nameof(pose)); + + NodeIndex = nodeIndex; + Key = key; + Pose = pose; + ParentNodeIndex = parentNodeIndex; + IncomingPrimitive = incomingPrimitive; + CurvaturePerMeter = curvaturePerMeter; + GCostMeters = gCostMeters; + HCostMeters = hCostMeters; + IsGoalCandidate = isGoalCandidate; + } + + /// 本次搜索节点数组中的稳定索引。 + public int NodeIndex { get; } + + /// 本节点用于 Open/Closed 状态管理的离散键。 + public HybridAStarNodeKey Key { get; } + + /// 未量化的连续车辆中心位姿。 + public Pose2D Pose { get; } + + /// 父节点稳定索引;根节点为 -1。 + public int ParentNodeIndex { get; } + + /// 由父节点驶入本节点的已连续碰撞检查原语;根节点为 null。 + public MotionPrimitive IncomingPrimitive { get; } + + /// 含义相同的原语别名。 + public MotionPrimitive Primitive { get { return IncomingPrimitive; } } + + /// 当前末段采用的车辆曲率,单位 1/m。 + public double CurvaturePerMeter { get; } + + /// 当前节点的累计等效米代价。 + public double GCostMeters { get; } + + /// 当前节点的二维启发式等效米代价。 + public double HCostMeters { get; } + + /// + /// 本节点是否由原语内部首次满足终点条件而生成。 + /// 终点候选必须保留连续位姿,不能因同一离散键的普通低代价节点而被 Open List 准入规则压制。 + /// + public bool IsGoalCandidate { get; } + + /// Open List 排序使用的总等效米代价。 + public double FCostMeters { get { return GCostMeters + HCostMeters; } } + + /// 本节点末段的行驶方向。 + public TravelDirection Direction { get { return Key.Direction; } } + + /// 本节点末段的曲率等级索引。 + public int CurvatureLevelIndex { get { return Key.CurvatureLevelIndex; } } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs new file mode 100644 index 0000000..e75342c --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs @@ -0,0 +1,67 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// Hybrid A* 闭集使用的离散状态键。 +/// 位置使用地图行列索引;航向、行驶方向和曲率等级共同保留车辆运动学状态,避免把同一栅格中的不同可达姿态错误合并。 +/// +public sealed class HybridAStarNodeKey : IEquatable +{ + /// + /// 创建一个离散 Hybrid A* 状态键。 + /// 参数:row、column 为零开始的地图行列;headingIndex 为航向桶;direction 为末段行驶方向;curvatureLevelIndex 为末段曲率等级。 + /// + public HybridAStarNodeKey(int row, int column, int headingIndex, TravelDirection direction, int curvatureLevelIndex) + { + Row = row; + Column = column; + HeadingIndex = headingIndex; + Direction = direction; + CurvatureLevelIndex = curvatureLevelIndex; + } + + /// 车辆中心所在的零开始地图行索引。 + public int Row { get; } + + /// 车辆中心所在的零开始地图列索引。 + public int Column { get; } + + /// 含义相同的列索引别名。 + public int Col { get { return Column; } } + + /// 根据配置航向分辨率量化后的航向桶索引。 + public int HeadingIndex { get; } + + /// 到达当前节点的最后一段行驶方向。 + public TravelDirection Direction { get; } + + /// 到达当前节点的最后一段曲率等级索引。 + public int CurvatureLevelIndex { get; } + + /// 判断另一个键是否表示完全相同的离散搜索状态。 + public bool Equals(HybridAStarNodeKey other) + { + return other != null && Row == other.Row && Column == other.Column && HeadingIndex == other.HeadingIndex && + Direction == other.Direction && CurvatureLevelIndex == other.CurvatureLevelIndex; + } + + /// 判断另一个对象是否表示完全相同的离散搜索状态。 + public override bool Equals(object obj) + { + return Equals(obj as HybridAStarNodeKey); + } + + /// 返回用于闭集字典的稳定哈希值。 + public override int GetHashCode() + { + unchecked + { + int hashCode = Row; + hashCode = hashCode * 397 ^ Column; + hashCode = hashCode * 397 ^ HeadingIndex; + hashCode = hashCode * 397 ^ (int)Direction; + return hashCode * 397 ^ CurvatureLevelIndex; + } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs new file mode 100644 index 0000000..cf05967 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs @@ -0,0 +1,542 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 单次 Hybrid A* 搜索的只读结果。 +/// 即使失败也会保留已经创建的运行期节点,供上层记录诊断;仅 有值。 +/// +public sealed class HybridAStarSearchResult +{ + internal HybridAStarSearchResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason) + { + Status = status; + Nodes = new ReadOnlyCollection(new List(nodes ?? Array.Empty())); + ExpandedNodeCount = expandedNodeCount; + GeneratedNodeCount = generatedNodeCount; + ReopenedNodeCount = reopenedNodeCount; + StaleOpenListEntryCount = staleOpenListEntryCount; + PeakOpenListCount = peakOpenListCount; + SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null; + TerminationReason = status == PlanningStatus.Success ? string.Empty : terminationReason ?? string.Empty; + } + + /// 搜索终止状态。 + public PlanningStatus Status { get; } + + /// 本次搜索已经创建的全部节点;索引与 一致。 + public IReadOnlyList Nodes { get; } + + /// 实际从 Open List 弹出并扩展的节点数量。 + public int ExpandedNodeCount { get; } + + /// 已进入 Open List 的节点数量,包含根节点和因改进代价追加的节点。 + public int GeneratedNodeCount { get; } + + /// 更优路径到达已关闭离散状态、并重新放回 Open List 的次数。 + public int ReopenedNodeCount { get; } + + /// 从 Open List 弹出后因已有更优普通状态而被丢弃的陈旧条目数量。 + public int StaleOpenListEntryCount { get; } + + /// 搜索期间 Open List 持有的最大条目数,包含等待惰性丢弃的旧条目。 + public int PeakOpenListCount { get; } + + /// 成功时最后一个从 Open List 弹出且满足终点条件的节点索引;失败时为 null。 + public int? SuccessNodeIndex { get; } + + /// 搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。 + public string TerminationReason { get; } +} + +/// +/// 在不可变 上执行前进/倒车恒曲率原语的 Hybrid A* 搜索。 +/// 搜索只消费已准备好的地图快照;所有候选原语均先完成连续扩大车体碰撞检查,再参与 Open List 排序。 +/// +public sealed class HybridAStarSearch +{ + private const double CostImprovementToleranceMeters = 1e-9d; + private readonly MotionPrimitiveGenerator _primitiveGenerator; + private readonly SearchCostCalculator _costCalculator; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认原语、代价和碰撞检查实现的搜索器。 + public HybridAStarSearch() + : this(new MotionPrimitiveGenerator(), new SearchCostCalculator(), new FootprintCollisionChecker()) + { + } + + /// 创建使用指定协作对象的搜索器,便于在不引入地图或 UI 依赖的情况下测试搜索过程。 + public HybridAStarSearch( + MotionPrimitiveGenerator primitiveGenerator, + SearchCostCalculator costCalculator, + FootprintCollisionChecker collisionChecker) + { + _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator)); + _costCalculator = costCalculator ?? throw new ArgumentNullException(nameof(costCalculator)); + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 执行一次不可变地图上的 Hybrid A* 搜索。 + /// 参数:request 提供地图、起终点、车辆和搜索配置;cancellationToken 在每次节点扩展前检查。 + /// 返回:成功仅在满足目标约束的候选已从 Open List 弹出时报告;任一失败状态均不报告成功节点。 + /// + public HybridAStarSearchResult Search(PlanningRequest request, CancellationToken cancellationToken) + { + TimeSpan timeout = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? request.Configuration.SearchTimeout + : TimeSpan.Zero; + PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero + ? new PlanningOperationBudget(cancellationToken, timeout) + : PlanningOperationBudget.Unlimited(cancellationToken); + return Search(request, budget); + } + + /// 使用门面传入的共享预算执行搜索;预算从整次规划开始计时。 + internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget) + { + var nodes = new List(); + int expandedNodeCount = 0; + int generatedNodeCount = 0; + int reopenedNodeCount = 0; + int staleOpenListEntryCount = 0; + int peakOpenListCount = 0; + + try + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + PlanningStatus validationStatus = ValidateRequest(request); + if (validationStatus != PlanningStatus.Success) + return CreateResult(validationStatus, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + PlanningGridMap map = request.Map; + HybridAStarConfiguration configuration = request.Configuration; + VehicleParameters vehicle = request.Vehicle; + + if (!IsFootprintInsideMap(request.Start, map, vehicle)) + return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!_collisionChecker.IsPoseCollisionFree(request.Start, map, vehicle, 0d, out _)) + return CreateResult(PlanningStatus.StartInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!IsFootprintInsideMap(request.Goal, map, vehicle)) + return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!_collisionChecker.IsPoseCollisionFree(request.Goal, map, vehicle, 0d, out _)) + return CreateResult(PlanningStatus.GoalInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (configuration.MaximumExpandedNodes == 0) + return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + if (!map.TryWorldToGrid(request.Goal.X, request.Goal.Y, out int goalRow, out int goalColumn)) + return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!GridDijkstraHeuristic.TryCreate(map, goalRow, goalColumn, budget, out GridDijkstraHeuristic heuristic, out stopReason)) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return CreateResult(PlanningStatus.InvalidVehicleParameters, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + IReadOnlyList curvatureLevels = _primitiveGenerator.GetCurvatureLevels(vehicle, configuration); + if (curvatureLevels.Count == 0) + return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + int headingBinCount = GetHeadingBinCount(configuration.HeadingResolutionRadians); + int startCurvatureLevelIndex = GetNearestCurvatureLevelIndex(curvatureLevels, request.StartVehicleCurvature); + if (startCurvatureLevelIndex < 0) + return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + if (!map.TryWorldToGrid(request.Start.X, request.Start.Y, out int startRow, out int startColumn)) + return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + var openList = new BinaryMinHeap(); + var bestStates = new Dictionary(); + foreach (TravelDirection startDirection in GetStartDirections(request, configuration)) + { + int headingIndex = AngleMath.ToHeadingIndex(request.Start.Heading, configuration.HeadingResolutionRadians, headingBinCount); + var key = new HybridAStarNodeKey(startRow, startColumn, headingIndex, startDirection, startCurvatureLevelIndex); + if (bestStates.ContainsKey(key)) continue; + + double hCostMeters = GetHeuristicCost(heuristic, startRow, startColumn, configuration); + if (double.IsPositiveInfinity(hCostMeters)) continue; + var node = new HybridAStarNode(nodes.Count, key, request.Start, -1, null, + curvatureLevels[startCurvatureLevelIndex], 0d, hCostMeters); + nodes.Add(node); + bestStates.Add(key, new NodeLabel(node.NodeIndex, 0d, false)); + openList.Push(node.NodeIndex, node.FCostMeters, node.HCostMeters, node.GCostMeters); + peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); + generatedNodeCount++; + } + + if (openList.Count == 0) + return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, + "二维启发式标记起点不可达目标,或起始方向无法进入 Open List。"); + + while (openList.Count > 0) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (expandedNodeCount >= configuration.MaximumExpandedNodes) + return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + + int nodeIndex = openList.Pop(); + HybridAStarNode current = nodes[nodeIndex]; + NodeLabel currentLabel = null; + if (!current.IsGoalCandidate) + { + if (!bestStates.TryGetValue(current.Key, out currentLabel) || currentLabel.NodeIndex != nodeIndex || currentLabel.IsClosed) + { + staleOpenListEntryCount++; + continue; + } + + currentLabel.IsClosed = true; + } + + expandedNodeCount++; + if (current.IsGoalCandidate) + { + if (!GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection) || + !_collisionChecker.IsPoseCollisionFree(current.Pose, map, vehicle, 0d, out _)) + continue; + + return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); + } + + if (GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection)) + return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, current.NodeIndex); + + for (int curvatureLevelIndex = 0; curvatureLevelIndex < curvatureLevels.Count; curvatureLevelIndex++) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) + return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null); + if (!MotionPrimitiveGenerator.AreCurvatureLevelsAdjacent(current.CurvatureLevelIndex, curvatureLevelIndex)) continue; + + foreach (TravelDirection direction in GetSuccessorDirections(configuration)) + { + MotionPrimitive primitive = _primitiveGenerator.Generate(current.Pose, curvatureLevels[curvatureLevelIndex], direction, request); + if (primitive == null || primitive.ActualLengthMeters <= 0d) continue; + + if (!map.TryWorldToGrid(primitive.End.X, primitive.End.Y, out int row, out int column)) continue; + double hCostMeters = GetHeuristicCost(heuristic, row, column, configuration); + if (double.IsPositiveInfinity(hCostMeters)) continue; + + double bodyClearanceMeters = GetMinimumBodyClearance(primitive); + bool isGearSwitch = current.ParentNodeIndex >= 0 && current.Direction != direction; + int curvatureLevelDelta = curvatureLevelIndex - current.CurvatureLevelIndex; + double incrementalCostMeters = _costCalculator.Calculate( + primitive.ActualLengthMeters, + direction, + isGearSwitch, + curvatureLevels[curvatureLevelIndex], + maximumCurvaturePerMeter, + curvatureLevelDelta, + bodyClearanceMeters, + configuration); + double gCostMeters = current.GCostMeters + incrementalCostMeters; + if (!NumericGuard.IsFinite(gCostMeters) || gCostMeters < 0d) continue; + + int headingIndex = AngleMath.ToHeadingIndex(primitive.End.Heading, configuration.HeadingResolutionRadians, headingBinCount); + var key = new HybridAStarNodeKey(row, column, headingIndex, direction, curvatureLevelIndex); + bestStates.TryGetValue(key, out NodeLabel existingLabel); + var successor = new HybridAStarNode(nodes.Count, key, primitive.End, current.NodeIndex, primitive, + curvatureLevels[curvatureLevelIndex], gCostMeters, hCostMeters, primitive.IsGoalTruncation); + if (existingLabel != null && !ShouldEnqueueSuccessor(successor, existingLabel.GCostMeters)) continue; + + if (!successor.IsGoalCandidate && existingLabel != null && existingLabel.IsClosed) reopenedNodeCount++; + nodes.Add(successor); + if (!successor.IsGoalCandidate) + bestStates[key] = new NodeLabel(successor.NodeIndex, gCostMeters, false); + openList.Push(successor.NodeIndex, successor.FCostMeters, successor.HCostMeters, successor.GCostMeters); + peakOpenListCount = Math.Max(peakOpenListCount, openList.Count); + generatedNodeCount++; + } + } + } + + return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, + "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"); + } + catch (Exception exception) + { + string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, null, reason); + } + } + + private static HybridAStarSearchResult CreateResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason = null) + { + string reason = status == PlanningStatus.Success + ? string.Empty + : terminationReason ?? GetDefaultTerminationReason(status); + return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason); + } + + private static string GetDefaultTerminationReason(PlanningStatus status) + { + switch (status) + { + case PlanningStatus.Cancelled: + return "Hybrid A* 搜索已取消。"; + case PlanningStatus.InvalidRequest: + return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。"; + case PlanningStatus.InvalidMap: + return "Hybrid A* 搜索地图结构无效。"; + case PlanningStatus.MapNotReady: + return "Hybrid A* 搜索地图尚未准备好。"; + case PlanningStatus.InvalidVehicleParameters: + return "Hybrid A* 搜索车辆参数无效。"; + case PlanningStatus.InvalidCurvatureConfiguration: + return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。"; + case PlanningStatus.StartOutsideMap: + return "Hybrid A* 搜索起始扩大车体不完全位于地图内。"; + case PlanningStatus.StartInCollision: + return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。"; + case PlanningStatus.GoalOutsideMap: + return "Hybrid A* 搜索目标扩大车体不完全位于地图内。"; + case PlanningStatus.GoalInCollision: + return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。"; + case PlanningStatus.SearchTimeout: + return "Hybrid A* 搜索使用的总规划预算已耗尽。"; + case PlanningStatus.SearchNodeLimitExceeded: + return "Hybrid A* 搜索达到扩展节点上限。"; + case PlanningStatus.NoFeasiblePath: + return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"; + case PlanningStatus.BacktrackingFailed: + return "Hybrid A* 成功节点无法回溯为完整父链。"; + case PlanningStatus.FinalValidationFailed: + return "Hybrid A* 路径未通过最终复核。"; + case PlanningStatus.InternalError: + return "Hybrid A* 搜索发生未预期内部错误。"; + default: + return "Hybrid A* 搜索以未识别状态终止:" + status + "。"; + } + } + + private static PlanningStatus ValidateRequest(PlanningRequest request) + { + if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || + !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || + !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) + return PlanningStatus.InvalidRequest; + + PlanningGridMap map = request.Map; + if (map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || map.Bounds == null) + return PlanningStatus.InvalidMap; + if (!map.PlanningReady) return PlanningStatus.MapNotReady; + + VehicleParameters vehicle = request.Vehicle; + if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || + !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return PlanningStatus.InvalidVehicleParameters; + + HybridAStarConfiguration configuration = request.Configuration; + if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || + (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) + return PlanningStatus.InvalidCurvatureConfiguration; + + return PlanningStatus.Success; + } + + private static bool IsValidConfiguration(HybridAStarConfiguration configuration) + { + return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && + NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && + NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && + NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && + configuration.HeadingResolutionRadians <= 2d * Math.PI && + configuration.CurvatureLevelCount >= 3 && configuration.CurvatureLevelCount % 2 == 1 && + NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && configuration.GoalPositionToleranceMeters >= 0d && + NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && configuration.GoalHeadingToleranceRadians >= 0d && + configuration.MaximumExpandedNodes >= 0 && configuration.SearchTimeout >= TimeSpan.Zero && + NumericGuard.IsFinite(configuration.HeuristicWeight) && configuration.HeuristicWeight >= 0d && + NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && + NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && + NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && + NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && + NumericGuard.IsFinite(configuration.ClearanceCostWeight) && configuration.ClearanceCostWeight >= 0d && + NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); + } + + private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) + { + if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; + + double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; + double longitudinalX = Math.Cos(pose.Heading); + double longitudinalY = Math.Sin(pose.Heading); + double lateralX = -longitudinalY; + double lateralY = longitudinalX; + for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) + for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) + { + double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; + double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + return true; + } + + private static IEnumerable GetStartDirections(PlanningRequest request, HybridAStarConfiguration configuration) + { + if (request.StartDirection.HasValue) + { + yield return request.StartDirection.Value; + yield break; + } + + yield return TravelDirection.Forward; + if (configuration.AllowReverse) yield return TravelDirection.Reverse; + } + + private static IEnumerable GetSuccessorDirections(HybridAStarConfiguration configuration) + { + yield return TravelDirection.Forward; + if (configuration.AllowReverse) yield return TravelDirection.Reverse; + } + + private static int GetHeadingBinCount(double headingResolutionRadians) + { + double rawBinCount = Math.Ceiling(2d * Math.PI / headingResolutionRadians); + if (!NumericGuard.IsFinite(rawBinCount) || rawBinCount < 1d || rawBinCount > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(headingResolutionRadians)); + return (int)rawBinCount; + } + + private static int GetNearestCurvatureLevelIndex(IReadOnlyList curvatureLevels, double curvaturePerMeter) + { + int bestIndex = -1; + double smallestDifference = double.PositiveInfinity; + for (int index = 0; index < curvatureLevels.Count; index++) + { + double difference = Math.Abs(curvatureLevels[index] - curvaturePerMeter); + if (difference >= smallestDifference) continue; + smallestDifference = difference; + bestIndex = index; + } + return bestIndex; + } + + private static double GetMinimumBodyClearance(MotionPrimitive primitive) + { + double minimum = double.PositiveInfinity; + foreach (double clearanceMeters in primitive.BodyClearancesMeters) + { + if (double.IsNaN(clearanceMeters) || clearanceMeters < 0d) return 0d; + minimum = Math.Min(minimum, clearanceMeters); + } + return minimum; + } + + private static double GetHeuristicCost(GridDijkstraHeuristic heuristic, int row, int column, HybridAStarConfiguration configuration) + { + double gridCostMeters = heuristic.GetCost(row, column); + if (double.IsPositiveInfinity(gridCostMeters)) return gridCostMeters; + double weightedCostMeters = gridCostMeters * configuration.HeuristicWeight; + return NumericGuard.IsFinite(weightedCostMeters) && weightedCostMeters >= 0d + ? weightedCostMeters + : double.PositiveInfinity; + } + + /// + /// 判断后继是否应进入 Open List。 + /// 参数:successor 是已完成连续碰撞检查的后继;bestKnownGCostMeters 是相同离散键普通状态的当前最优 G 值。 + /// 返回:普通状态仅在严格改善最优 G 值时入队;终点候选始终入队,以保留其未量化的连续终点位姿。 + /// + private static bool ShouldEnqueueSuccessor(HybridAStarNode successor, double bestKnownGCostMeters) + { + if (successor == null || !NumericGuard.IsFinite(bestKnownGCostMeters)) return false; + return successor.IsGoalCandidate || successor.GCostMeters < bestKnownGCostMeters - CostImprovementToleranceMeters; + } + + private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) + { + if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; + if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } + + private sealed class NodeLabel + { + public NodeLabel(int nodeIndex, double gCostMeters, bool isClosed) + { + NodeIndex = nodeIndex; + GCostMeters = gCostMeters; + IsClosed = isClosed; + } + + public int NodeIndex { get; } + public double GCostMeters { get; } + public bool IsClosed { get; set; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs new file mode 100644 index 0000000..fba41ed --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 一条经连续碰撞检查的恒曲率运动原语。 +/// 位置和长度单位为 m,航向单位为 rad,曲率单位为 1/m; 不包含起点,只包含按积分顺序产生的后续点。 +/// +public sealed class MotionPrimitive +{ + /// + /// 创建不可变运动原语。 + /// 参数:start 为原语起点;direction 为行驶方向;curvaturePerMeter 为恒定曲率;actualLengthMeters 为已实际行驶长度; + /// points 与 bodyClearancesMeters 按同一索引保存内部积分位姿和对应的车体净空;isGoalTruncation 表示末点是否首次命中目标容差。 + /// + public MotionPrimitive( + Pose2D start, + TravelDirection direction, + double curvaturePerMeter, + double actualLengthMeters, + IEnumerable points, + IEnumerable bodyClearancesMeters, + bool isGoalTruncation) + { + if (start == null) throw new ArgumentNullException(nameof(start)); + if (points == null) throw new ArgumentNullException(nameof(points)); + if (bodyClearancesMeters == null) throw new ArgumentNullException(nameof(bodyClearancesMeters)); + + var copiedPoints = new List(points); + var copiedClearances = new List(bodyClearancesMeters); + if (copiedPoints.Count != copiedClearances.Count) + throw new ArgumentException("The point and clearance counts must match.", nameof(bodyClearancesMeters)); + + Start = start; + Direction = direction; + CurvaturePerMeter = curvaturePerMeter; + ActualLengthMeters = actualLengthMeters; + Points = new ReadOnlyCollection(copiedPoints); + BodyClearancesMeters = new ReadOnlyCollection(copiedClearances); + End = copiedPoints.Count == 0 ? start : copiedPoints[copiedPoints.Count - 1]; + IsGoalTruncation = isGoalTruncation; + } + + /// 原语起始车辆几何中心位姿。 + public Pose2D Start { get; } + + /// 原语最后一个积分点;零长度终点候选时等于 + public Pose2D End { get; } + + /// 原语对应的行驶方向。 + public TravelDirection Direction { get; } + + /// 原语全程采用的恒定车辆曲率,单位 1/m。 + public double CurvaturePerMeter { get; } + + /// 的实际行驶弧长,单位 m。 + public double ActualLengthMeters { get; } + + /// 不含起点的连续积分位姿,只读且按行驶顺序排列。 + public IReadOnlyList Points { get; } + + /// 一一对应的扩大车体保守净空下界,单位 m。 + public IReadOnlyList BodyClearancesMeters { get; } + + /// 末点是否因首次满足目标位置、航向和方向约束而截断。 + public bool IsGoalTruncation { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs new file mode 100644 index 0000000..817cc58 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 生成经解析积分和连续碰撞检查的前进或倒车恒曲率原语。 +/// 每个积分点均依次经过有限数值检查、与前一点之间的扫掠碰撞检查和终点容差检查。 +/// +public sealed class MotionPrimitiveGenerator +{ + private const double StraightCurvatureThreshold = 1e-12d; + private readonly FootprintCollisionChecker _collisionChecker; + + /// 创建使用默认车辆连续碰撞检查器的原语生成器。 + public MotionPrimitiveGenerator() + : this(new FootprintCollisionChecker()) + { + } + + /// 创建使用指定连续车辆碰撞检查器的原语生成器。 + public MotionPrimitiveGenerator(FootprintCollisionChecker collisionChecker) + { + _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); + } + + /// + /// 使用完整规划请求生成一条原语。 + /// 参数:start 为当前连续位姿;curvaturePerMeter 为候选恒定曲率;direction 为前进或倒车;request 提供地图、车辆、配置和目标。 + /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;否则返回最大长度不超过配置上限的原语。 + /// + public MotionPrimitive Generate(Pose2D start, double curvaturePerMeter, TravelDirection direction, PlanningRequest request) + { + if (request == null) return null; + return Generate(start, curvaturePerMeter, direction, request.Map, request.Vehicle, request.Configuration, request.Goal, request.GoalDirection); + } + + /// + /// 使用显式地图、车辆、配置和目标生成一条原语。 + /// 参数:所有位置使用 m/rad,curvaturePerMeter 使用 1/m;goalDirection 限制末段允许的进入方向。 + /// 返回:输入无效、曲率超限或任一积分点碰撞时为 null;首次命中目标时返回 为 true 的截断原语。 + /// + public MotionPrimitive Generate( + Pose2D start, + double curvaturePerMeter, + TravelDirection direction, + PlanningGridMap map, + VehicleParameters vehicle, + HybridAStarConfiguration configuration, + Pose2D goal, + GoalDirectionConstraint goalDirection) + { + if (!IsValidInput(start, curvaturePerMeter, direction, map, vehicle, configuration, goal, goalDirection)) return null; + + if (GoalToleranceChecker.IsSatisfied(start, goal, configuration, direction, goalDirection)) + return new MotionPrimitive(start, direction, curvaturePerMeter, 0d, Array.Empty(), Array.Empty(), true); + + double pointStepMeters = Math.Min(configuration.IntegrationStepMeters, + Math.Min(configuration.MaximumCollisionCheckStepMeters, map.ResolutionMeters / 2d)); + if (!NumericGuard.IsPositiveFinite(pointStepMeters)) return null; + + var points = new List(); + var bodyClearancesMeters = new List(); + Pose2D previous = start; + double actualLengthMeters = 0d; + while (actualLengthMeters < configuration.PrimitiveLengthMeters) + { + double remainingLengthMeters = configuration.PrimitiveLengthMeters - actualLengthMeters; + double stepMeters = Math.Min(pointStepMeters, remainingLengthMeters); + if (!NumericGuard.IsPositiveFinite(stepMeters)) return null; + + Pose2D next = Integrate(previous, curvaturePerMeter, direction, stepMeters); + if (!IsFinitePose(next)) return null; + if (!_collisionChecker.IsSweptMotionCollisionFree(previous, next, map, vehicle, stepMeters, out double bodyClearanceMeters)) return null; + + actualLengthMeters += stepMeters; + points.Add(next); + bodyClearancesMeters.Add(bodyClearanceMeters); + if (GoalToleranceChecker.IsSatisfied(next, goal, configuration, direction, goalDirection)) + return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, true); + + previous = next; + } + + return new MotionPrimitive(start, direction, curvaturePerMeter, actualLengthMeters, points, bodyClearancesMeters, false); + } + + /// + /// 获取从最大负曲率到最大正曲率均匀分布的候选曲率等级。 + /// 参数:vehicle 提供保守最大曲率;configuration 的曲率等级数必须为不小于 3 的奇数。 + /// 返回:输入无效时为空只读列表;有效时长度等于配置等级数且中间等级恒为零曲率。 + /// + public IReadOnlyList GetCurvatureLevels(VehicleParameters vehicle, HybridAStarConfiguration configuration) + { + if (configuration == null || configuration.CurvatureLevelCount < 3 || configuration.CurvatureLevelCount % 2 == 0 || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return Array.Empty(); + + var levels = new double[configuration.CurvatureLevelCount]; + double increment = 2d * maximumCurvaturePerMeter / (levels.Length - 1d); + for (int index = 0; index < levels.Length; index++) levels[index] = -maximumCurvaturePerMeter + increment * index; + levels[levels.Length / 2] = 0d; + return levels; + } + + /// + /// 判断两个曲率等级是否允许在相邻原语间直接切换。 + /// 参数:previousLevelIndex 与 nextLevelIndex 为从零开始的等级索引。 + /// 返回:两个索引均非负且最多相差一个等级时为 true。 + /// + public static bool AreCurvatureLevelsAdjacent(int previousLevelIndex, int nextLevelIndex) + { + return previousLevelIndex >= 0 && nextLevelIndex >= 0 && Math.Abs(previousLevelIndex - nextLevelIndex) <= 1; + } + + private static bool IsValidInput( + Pose2D start, + double curvaturePerMeter, + TravelDirection direction, + PlanningGridMap map, + VehicleParameters vehicle, + HybridAStarConfiguration configuration, + Pose2D goal, + GoalDirectionConstraint goalDirection) + { + if (!IsFinitePose(start) || !IsFinitePose(goal) || map == null || vehicle == null || configuration == null || + !NumericGuard.IsFinite(curvaturePerMeter) || !IsTravelDirection(direction) || !IsGoalDirection(goalDirection) || + !NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) || + !NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) || + !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || + !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) + return false; + + return Math.Abs(curvaturePerMeter) <= maximumCurvaturePerMeter; + } + + private static Pose2D Integrate(Pose2D previous, double curvaturePerMeter, TravelDirection direction, double stepMeters) + { + double signedDistanceMeters = direction == TravelDirection.Forward ? stepMeters : -stepMeters; + double nextHeadingRadians = AngleMath.NormalizeRadians(previous.Heading + curvaturePerMeter * signedDistanceMeters); + if (Math.Abs(curvaturePerMeter) < StraightCurvatureThreshold) + { + return new Pose2D( + previous.X + signedDistanceMeters * Math.Cos(previous.Heading), + previous.Y + signedDistanceMeters * Math.Sin(previous.Heading), + nextHeadingRadians); + } + + return new Pose2D( + previous.X + (Math.Sin(nextHeadingRadians) - Math.Sin(previous.Heading)) / curvaturePerMeter, + previous.Y - (Math.Cos(nextHeadingRadians) - Math.Cos(previous.Heading)) / curvaturePerMeter, + nextHeadingRadians); + } + + 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 IsGoalDirection(GoalDirectionConstraint direction) + { + return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs new file mode 100644 index 0000000..00babd0 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs @@ -0,0 +1,98 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search; + +/// +/// 将运动原语的长度、方向、曲率和净空转换为统一的等效米搜索代价。 +/// 此类型不修改搜索状态;换向标记必须由调用方依据相邻原语的方向关系提供。 +/// +public sealed class SearchCostCalculator +{ + /// 创建使用调用时配置参数的等效米代价计算器。 + public SearchCostCalculator() + { + } + + /// + /// 计算一条运动原语的等效米增量代价。 + /// 参数:lengthMeters 为非负弧长 m;direction 为原语方向;isGearSwitch 表示该原语前是否换向; + /// curvaturePerMeter 与 maximumCurvaturePerMeter 的单位为 1/m;curvatureLevelDelta 为相邻曲率等级差; + /// bodyClearanceMeters 为非负车体保守净空 m,可为正无穷;configuration 提供非负权重和惩罚。 + /// 返回:有限且非负的等效米代价。 + /// 失败:任一数值、方向或权重无效时抛出 。 + /// + public double Calculate( + double lengthMeters, + TravelDirection direction, + bool isGearSwitch, + double curvaturePerMeter, + double maximumCurvaturePerMeter, + int curvatureLevelDelta, + double bodyClearanceMeters, + HybridAStarConfiguration configuration) + { + ValidateInput(lengthMeters, direction, curvaturePerMeter, maximumCurvaturePerMeter, bodyClearanceMeters, configuration); + + double directionMultiplier = direction == TravelDirection.Reverse ? configuration.ReverseCostMultiplier : 1d; + double normalizedCurvature = Math.Abs(curvaturePerMeter / maximumCurvaturePerMeter); + double clearanceDeficit = GetClearanceDeficit(bodyClearanceMeters, configuration.ClearanceCostDistanceMeters); + double levelDelta = Math.Abs((double)curvatureLevelDelta); + double motionCost = lengthMeters * directionMultiplier * + (1d + configuration.CurvatureMagnitudeWeight * normalizedCurvature + + configuration.ClearanceCostWeight * clearanceDeficit); + double gearSwitchCost = isGearSwitch ? configuration.GearSwitchPenaltyMeters : 0d; + double curvatureChangeCost = configuration.CurvatureChangePenaltyMetersPerLevel * levelDelta; + double totalCost = motionCost + gearSwitchCost + curvatureChangeCost; + if (!NumericGuard.IsFinite(totalCost) || totalCost < 0d) + throw new ArgumentOutOfRangeException(nameof(lengthMeters), "The calculated cost must remain finite and non-negative."); + + return totalCost; + } + + private static void ValidateInput( + double lengthMeters, + TravelDirection direction, + double curvaturePerMeter, + double maximumCurvaturePerMeter, + double bodyClearanceMeters, + HybridAStarConfiguration configuration) + { + if (!NumericGuard.IsFinite(lengthMeters) || lengthMeters < 0d) + throw new ArgumentOutOfRangeException(nameof(lengthMeters)); + if (direction != TravelDirection.Forward && direction != TravelDirection.Reverse) + throw new ArgumentOutOfRangeException(nameof(direction)); + if (!NumericGuard.IsFinite(curvaturePerMeter)) + throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); + if (!NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter)) + throw new ArgumentOutOfRangeException(nameof(maximumCurvaturePerMeter)); + if (Math.Abs(curvaturePerMeter) > maximumCurvaturePerMeter) + throw new ArgumentOutOfRangeException(nameof(curvaturePerMeter)); + if (double.IsNaN(bodyClearanceMeters) || bodyClearanceMeters < 0d) + throw new ArgumentOutOfRangeException(nameof(bodyClearanceMeters)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + ValidateNonNegativeFinite(configuration.HeuristicWeight, nameof(configuration.HeuristicWeight)); + if (!NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier)) + throw new ArgumentOutOfRangeException(nameof(configuration.ReverseCostMultiplier)); + ValidateNonNegativeFinite(configuration.GearSwitchPenaltyMeters, nameof(configuration.GearSwitchPenaltyMeters)); + ValidateNonNegativeFinite(configuration.CurvatureMagnitudeWeight, nameof(configuration.CurvatureMagnitudeWeight)); + ValidateNonNegativeFinite(configuration.CurvatureChangePenaltyMetersPerLevel, nameof(configuration.CurvatureChangePenaltyMetersPerLevel)); + ValidateNonNegativeFinite(configuration.ClearanceCostWeight, nameof(configuration.ClearanceCostWeight)); + if (!NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters)) + throw new ArgumentOutOfRangeException(nameof(configuration.ClearanceCostDistanceMeters)); + } + + private static void ValidateNonNegativeFinite(double value, string parameterName) + { + if (!NumericGuard.IsFinite(value) || value < 0d) + throw new ArgumentOutOfRangeException(parameterName); + } + + private static double GetClearanceDeficit(double bodyClearanceMeters, double clearanceCostDistanceMeters) + { + if (double.IsPositiveInfinity(bodyClearanceMeters)) return 0d; + double deficit = 1d - bodyClearanceMeters / clearanceCostDistanceMeters; + return deficit > 0d ? deficit : 0d; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs new file mode 100644 index 0000000..63872e2 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; + +/// Clumsy 粗路径手动测试可选择的固定场景。 +public enum CoarsePathTestScenario +{ + /// 明确允许的空地图直达场景。 + ExplicitEmpty, + + /// 由中央矩形阻断直线的绕行场景。 + RectangleDetour, + + /// 同时包含手工圆形、矩形与 TwoLeg 快照的多来源场景。 + ManualAndTwoLeg, + + /// 与矩形绕行输入完全一致,用于在同一服务中验证输入缓存命中。 + CacheHit, + + /// 起步前进、终点倒车进入的换向场景。 + ReverseGearSwitch, + + /// 由贯穿边界的障碍带分隔起终点的无解场景。 + NoFeasiblePath, +} + +/// 手动障碍物输入支持的世界几何类型。 +public enum ManualCoarsePathObstacleKind +{ + /// 由世界中心和半径定义的圆形障碍物。 + Circle, + + /// 由世界中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。 + AxisAlignedRectangle, +} + +/// +/// 手动粗路径测试的不可变障碍物输入。 +/// 所有中心和尺寸均使用世界 mm;矩形始终与世界坐标轴平行,不包含旋转角。 +/// +public sealed class ManualCoarsePathObstacle +{ + private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters, + double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters) + { + Kind = kind; + CenterXMillimeters = centerXMillimeters; + CenterYMillimeters = centerYMillimeters; + SizeXMillimeters = sizeXMillimeters; + SizeYMillimeters = sizeYMillimeters; + } + + /// 障碍物的支持几何类型。 + public ManualCoarsePathObstacleKind Kind { get; } + + /// 几何中心世界 X 坐标,单位 mm。 + public double CenterXMillimeters { get; } + + /// 几何中心世界 Y 坐标,单位 mm。 + public double CenterYMillimeters { get; } + + /// 圆形时为半径,矩形时为 X 方向长度;单位 mm。 + public double SizeXMillimeters { get; } + + /// 圆形时为半径,矩形时为 Y 方向宽度;单位 mm。 + public double SizeYMillimeters { get; } + + /// 创建圆形障碍物。参数:圆心和半径均使用世界 mm,半径必须为有限正数。 + public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters, + double radiusMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters, + centerYMillimeters, radiusMillimeters, radiusMillimeters); + } + + /// + /// 创建轴对齐矩形障碍物。 + /// 参数:中心、X 方向长度和 Y 方向宽度均使用世界 mm;两个尺寸必须为有限正数。 + /// + public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters, + double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters)); + EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle, + centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters); + } + + private static void EnsureFinite(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); + } + + private static void EnsurePositiveFinite(double value, string parameterName) + { + EnsureFinite(value, parameterName); + if (value <= 0d) + throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); + } +} + +/// +/// Clumsy 粗路径测试的纯输入工厂。 +/// 固定场景不读取 UI、传感器、定位或时钟;传入 AMR 位姿的手动入口仅在此处完成世界 mm/deg 到核心 m/rad 的转换。 +/// +public static class CoarsePathScenarioFactory +{ + private const float MapXMinMillimeters = 0f; + private const float MapXMaxMillimeters = 6000f; + private const float MapYMinMillimeters = 0f; + private const float MapYMaxMillimeters = 4000f; + private const float ResolutionMillimeters = 50f; + private const double MillimetersPerMeter = 1000d; + private const double DegreesToRadians = Math.PI / 180d; + private const double ManualMapPaddingMillimeters = 8000d; + private const int MaximumManualObstacleCount = 20; + + /// + /// 创建一个新的固定测试业务请求。 + /// 返回:每次调用都返回独立的可变请求对象,供调用方安全地传入同一个长期存活的规划服务。 + /// + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) + { + return CreateCore(scenario, null); + } + + /// + /// 创建以当前 AMR 世界位姿为起点的固定测试业务请求。 + /// 参数:X/Y 使用世界 mm,航向使用 deg;地图、目标和障碍物仅随 AMR 坐标平移,TwoLeg 朝向保持不变。 + /// + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, + double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees) + { + return CreateCore(scenario, + new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees)); + } + + private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, FixedScenarioAnchor anchor) + { + switch (scenario) + { + case CoarsePathTestScenario.ExplicitEmpty: + return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.RectangleDetour: + case CoarsePathTestScenario.CacheHit: + return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.ManualAndTwoLeg: + return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor)); + case CoarsePathTestScenario.ReverseGearSwitch: + return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.NoFeasiblePath: + return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + default: + throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + + /// + /// 创建传入 AMR 世界位姿和手动世界终点的空图演示请求。 + /// 参数:X/Y 使用世界 mm,航向使用 deg;返回请求中的 使用世界 m/rad。 + /// 注意:这是坐标、路径和取消流程的演示空图,不能表示现场不存在障碍物。 + /// + public static CoarsePathPlanningJob CreateManualGoalDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) + { + return CreateManualObstacleDemo(startXMillimeters, startYMillimeters, startHeadingDegrees, + goalXMillimeters, goalYMillimeters, goalHeadingDegrees, + Array.Empty(), 0L); + } + + /// + /// 创建传入 AMR 世界位姿、手动世界终点和手动障碍物快照的测试请求。 + /// 参数:位姿 X/Y、障碍物中心和尺寸使用世界 mm,航向使用 deg;返回的 使用 m/rad。 + /// 障碍物非空时 obstacleSnapshotVersion 必须为正数,以避免长期服务错误复用旧地图;零障碍物才创建显式空图演示。 + /// + public static CoarsePathPlanningJob CreateManualObstacleDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees, + IReadOnlyList obstacles, long obstacleSnapshotVersion) + { + EnsureFinite(startXMillimeters, nameof(startXMillimeters)); + EnsureFinite(startYMillimeters, nameof(startYMillimeters)); + EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); + EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); + EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); + EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); + + if (obstacles == null) throw new ArgumentNullException(nameof(obstacles)); + if (obstacles.Count > MaximumManualObstacleCount) + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle count exceeds the supported limit."); + if (obstacles.Count != 0 && obstacleSnapshotVersion <= 0L) + throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version."); + + return CreateJob( + CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters, + obstacles, obstacleSnapshotVersion), + ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), + ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), + null, + GoalDirectionConstraint.Any); + } + + private static CoarsePathPlanningJob CreateExplicitEmpty(FixedScenarioTransform transform) + { + return CreateJob( + CreateMap(true, Array.Empty(), transform), + transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), + null, + GoalDirectionConstraint.Forward); + } + + private static CoarsePathPlanningJob CreateRectangleDetour(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] + { + new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f), + transform.Y(1200f), transform.Y(2800f)), + }), + }; + CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); + // 固定绕行场景保留最优启发式;30 秒覆盖较慢测试环境,UI 仍可随时取消。 + job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d); + return job; + } + + private static CoarsePathPlanningJob CreateManualAndTwoLeg(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 2L, true, new IMapObstacle[] + { + new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f), + new AxisAlignedRectangleObstacle(transform.X(3000f), transform.X(3600f), + transform.Y(2000f), transform.Y(2600f)), + }), + new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true, + transform.X(3900f), transform.Y(2500f), 0d, + -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), + }; + CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 1d, 0d), + transform.Pose(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); + // 多来源场景的最优绕行会受机器负载影响;放宽演示总预算但保留全部碰撞与目标判定。 + job.Configuration.SearchTimeout = TimeSpan.FromSeconds(15d); + return job; + } + + private static CoarsePathPlanningJob CreateReverseGearSwitch(FixedScenarioTransform transform) + { + return CreateJob( + CreateMap(true, Array.Empty(), transform), + transform.Pose(1d, 2d, 0d), + transform.Pose(4d, 2d, 0d), + TravelDirection.Forward, + GoalDirectionConstraint.Reverse); + } + + private static CoarsePathPlanningJob CreateNoFeasiblePath(FixedScenarioTransform transform) + { + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual", 3L, true, new IMapObstacle[] + { + new AxisAlignedRectangleObstacle(transform.X(2900f), transform.X(3100f), + transform.Y(0f), transform.Y(4000f)), + }), + }; + return CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d), + transform.Pose(5d, 2d, 0d), + null, GoalDirectionConstraint.Forward); + } + + private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, + TravelDirection? startDirection, GoalDirectionConstraint goalDirection) + { + return new CoarsePathPlanningJob + { + MapRequest = mapRequest, + Start = start, + Goal = goal, + Vehicle = new VehicleParameters + { + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, + }, + Configuration = new HybridAStarConfiguration(), + StartDirection = startDirection, + GoalDirection = goalDirection, + }; + } + + private static PlanningMapRequest CreateMap(bool allowExplicitEmptyMap, IReadOnlyList sources, + FixedScenarioTransform transform) + { + return new PlanningMapRequest + { + Bounds = new MapBoundsMm(transform.X(MapXMinMillimeters), transform.X(MapXMaxMillimeters), + transform.Y(MapYMinMillimeters), transform.Y(MapYMaxMillimeters)), + ResolutionMm = ResolutionMillimeters, + ObstacleSources = sources, + AllowExplicitEmptyMap = allowExplicitEmptyMap, + }; + } + + private sealed class FixedScenarioAnchor + { + public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees) + { + EnsureFinite(xMillimeters, nameof(xMillimeters)); + EnsureFinite(yMillimeters, nameof(yMillimeters)); + EnsureFinite(headingDegrees, nameof(headingDegrees)); + XMillimeters = xMillimeters; + YMillimeters = yMillimeters; + HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians); + } + + public double XMillimeters { get; } + public double YMillimeters { get; } + public double HeadingRadians { get; } + } + + private sealed class FixedScenarioTransform + { + private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians) + { + DeltaXMillimeters = deltaXMillimeters; + DeltaYMillimeters = deltaYMillimeters; + HeadingDeltaRadians = headingDeltaRadians; + } + + public double DeltaXMillimeters { get; } + public double DeltaYMillimeters { get; } + public double HeadingDeltaRadians { get; } + + public static FixedScenarioTransform From(double baselineStartXMillimeters, + double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor) + { + if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d); + return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters, + anchor.YMillimeters - baselineStartYMillimeters, + NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians)); + } + + public float X(float value) + { + return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); + } + + public float Y(float value) + { + return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); + } + + public Pose2D Pose(double xMeters, double yMeters, double headingRadians) + { + return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter, + (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter, + NormalizeRadians(headingRadians + HeadingDeltaRadians)); + } + } + + private static PlanningMapRequest CreateManualDemoMap(double startXMillimeters, double startYMillimeters, + double goalXMillimeters, double goalYMillimeters, IReadOnlyList obstacles, + long obstacleSnapshotVersion) + { + double minimumX = Math.Min(startXMillimeters, goalXMillimeters); + double maximumX = Math.Max(startXMillimeters, goalXMillimeters); + double minimumY = Math.Min(startYMillimeters, goalYMillimeters); + double maximumY = Math.Max(startYMillimeters, goalYMillimeters); + for (int index = 0; index < obstacles.Count; index++) + { + ManualCoarsePathObstacle obstacle = obstacles[index] ?? + throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); + double halfX; + double halfY; + switch (obstacle.Kind) + { + case ManualCoarsePathObstacleKind.Circle: + EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); + halfX = obstacle.SizeXMillimeters; + halfY = obstacle.SizeYMillimeters; + break; + case ManualCoarsePathObstacleKind.AxisAlignedRectangle: + EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles)); + EnsurePositiveFinite(obstacle.SizeYMillimeters, nameof(obstacles)); + halfX = obstacle.SizeXMillimeters / 2d; + halfY = obstacle.SizeYMillimeters / 2d; + break; + default: + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); + } + + minimumX = Math.Min(minimumX, obstacle.CenterXMillimeters - halfX); + maximumX = Math.Max(maximumX, obstacle.CenterXMillimeters + halfX); + minimumY = Math.Min(minimumY, obstacle.CenterYMillimeters - halfY); + maximumY = Math.Max(maximumY, obstacle.CenterYMillimeters + halfY); + } + + float xMin = ToGridLowerBound(minimumX - ManualMapPaddingMillimeters); + float xMax = ToGridUpperBound(maximumX + ManualMapPaddingMillimeters); + float yMin = ToGridLowerBound(minimumY - ManualMapPaddingMillimeters); + float yMax = ToGridUpperBound(maximumY + ManualMapPaddingMillimeters); + bool isExplicitEmptyMap = obstacles.Count == 0; + return new PlanningMapRequest + { + Bounds = new MapBoundsMm(xMin, xMax, yMin, yMax), + ResolutionMm = ResolutionMillimeters, + ObstacleSources = isExplicitEmptyMap ? Array.Empty() : + new IMapObstacleSource[] + { + new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, + ConvertManualObstacles(obstacles)), + }, + AllowExplicitEmptyMap = isExplicitEmptyMap, + }; + } + + private static IMapObstacle[] ConvertManualObstacles(IReadOnlyList obstacles) + { + var result = new IMapObstacle[obstacles.Count]; + for (int index = 0; index < obstacles.Count; index++) + { + ManualCoarsePathObstacle obstacle = obstacles[index] ?? + throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles)); + switch (obstacle.Kind) + { + case ManualCoarsePathObstacleKind.Circle: + result[index] = new CircleObstacle(ToFiniteFloat(obstacle.CenterXMillimeters, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters, nameof(obstacles)), + ToFiniteFloat(obstacle.SizeXMillimeters, nameof(obstacles))); + break; + case ManualCoarsePathObstacleKind.AxisAlignedRectangle: + double halfX = obstacle.SizeXMillimeters / 2d; + double halfY = obstacle.SizeYMillimeters / 2d; + result[index] = new AxisAlignedRectangleObstacle( + ToFiniteFloat(obstacle.CenterXMillimeters - halfX, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterXMillimeters + halfX, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters - halfY, nameof(obstacles)), + ToFiniteFloat(obstacle.CenterYMillimeters + halfY, nameof(obstacles))); + break; + default: + throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported."); + } + } + return result; + } + + private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) + { + return new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, + headingDegrees * DegreesToRadians); + } + + private static double NormalizeRadians(double angle) + { + double normalized = angle % (2d * Math.PI); + if (normalized <= -Math.PI) return normalized + 2d * Math.PI; + return normalized > Math.PI ? normalized - 2d * Math.PI : normalized; + } + + private static float ToGridLowerBound(double millimeters) + { + double rounded = Math.Floor(millimeters / ResolutionMillimeters) * ResolutionMillimeters; + return ToFiniteFloat(rounded, nameof(millimeters)); + } + + private static float ToGridUpperBound(double millimeters) + { + double rounded = Math.Ceiling(millimeters / ResolutionMillimeters) * ResolutionMillimeters; + return ToFiniteFloat(rounded, nameof(millimeters)); + } + + private static float ToFiniteFloat(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue) + throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate."); + return (float)value; + } + + private static void EnsureFinite(double value, string parameterName) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); + } + + private static void EnsurePositiveFinite(double value, string parameterName) + { + EnsureFinite(value, parameterName); + if (value <= 0d) + throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs new file mode 100644 index 0000000..2fa47d5 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs @@ -0,0 +1,628 @@ +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 + " 必须是有限数。"); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs new file mode 100644 index 0000000..c403d4a --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs @@ -0,0 +1,160 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// +/// 对以车辆几何中心表示的扩大矩形执行连续碰撞检查。 +/// 地图查询使用 m;任何地图外车辆部分、占据格相交或擦边均按碰撞处理。 +/// +public sealed class FootprintCollisionChecker +{ + /// 创建连续车辆碰撞检查器。 + public FootprintCollisionChecker() + { + } + + /// + /// 判断单个车辆位姿是否无碰撞。 + /// 参数:pose 为车辆几何中心的世界 m/rad 位姿;map 为不可变规划地图;vehicle 为车辆尺寸; + /// additionalMarginMeters 为临时额外安全余量,单位 m;bodyClearanceMeters 输出不含该临时余量的保守车体净空下界,单位 m。 + /// 返回:扩大车辆矩形完整位于地图内且不与任何占据格相交或擦边时为 true;无效输入保守地返回 false。 + /// + public bool IsPoseCollisionFree(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, + double additionalMarginMeters, out double bodyClearanceMeters) + { + bodyClearanceMeters = 0d; + if (map == null || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d || + !VehicleFootprint.TryCreate(pose, vehicle, 0d, out VehicleFootprint bodyFootprint) || + !VehicleFootprint.TryCreate(pose, vehicle, additionalMarginMeters, out VehicleFootprint checkedFootprint)) + return false; + + if (!AreCornersInsideMap(checkedFootprint, map)) return false; + + double centerDistanceMeters = map.GetConservativeObstacleDistanceMeters(pose.X, pose.Y); + bodyClearanceMeters = GetBodyClearance(centerDistanceMeters, bodyFootprint.CircumscribedRadiusMeters); + if (centerDistanceMeters > checkedFootprint.CircumscribedRadiusMeters) return true; + + return !IntersectsOccupiedCell(checkedFootprint, map); + } + + /// + /// 判断两个位姿之间的平移和转向扫掠是否无碰撞。 + /// 参数:from、to 为世界 m/rad 位姿;maximumCenterStepMeters 为允许的最大中心采样间距,单位 m; + /// minimumBodyClearanceMeters 输出沿途不含临时扫掠余量的保守车体净空下界,单位 m。 + /// 返回:端点和每个分段扫掠均无碰撞时为 true;无效输入、地图外或任一中间碰撞时返回 false。 + /// + public bool IsSweptMotionCollisionFree(Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, + double maximumCenterStepMeters, out double minimumBodyClearanceMeters) + { + minimumBodyClearanceMeters = 0d; + if (map == null || from == null || to == null || !NumericGuard.IsFinite(maximumCenterStepMeters) || maximumCenterStepMeters <= 0d || + !NumericGuard.IsFinite(from.X) || !NumericGuard.IsFinite(from.Y) || !NumericGuard.IsFinite(from.Heading) || + !NumericGuard.IsFinite(to.X) || !NumericGuard.IsFinite(to.Y) || !NumericGuard.IsFinite(to.Heading) || + !VehicleFootprint.TryCreate(from, vehicle, 0d, out VehicleFootprint bodyFootprint)) + return false; + + double allowedStepMeters = Math.Min(maximumCenterStepMeters, map.ResolutionMeters / 2d); + if (!NumericGuard.IsPositiveFinite(allowedStepMeters)) return false; + + if (!IsPoseCollisionFree(from, map, vehicle, 0d, out double fromClearanceMeters)) return false; + minimumBodyClearanceMeters = fromClearanceMeters; + + double deltaX = to.X - from.X; + double deltaY = to.Y - from.Y; + double centerDistanceMeters = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + if (!NumericGuard.IsFinite(centerDistanceMeters)) return false; + double headingDeltaRadians = AngleMath.ShortestSignedDifference(from.Heading, to.Heading); + if (!NumericGuard.IsFinite(headingDeltaRadians)) return false; + double rawSegmentCount = Math.Ceiling(centerDistanceMeters / allowedStepMeters); + if (!NumericGuard.IsFinite(rawSegmentCount) || rawSegmentCount > int.MaxValue) return false; + int segmentCount = Math.Max(1, (int)rawSegmentCount); + + Pose2D previousPose = from; + for (int segment = 1; segment <= segmentCount; segment++) + { + double endFraction = (double)segment / segmentCount; + double middleFraction = ((double)segment - 0.5d) / segmentCount; + var currentPose = new Pose2D( + from.X + deltaX * endFraction, + from.Y + deltaY * endFraction, + from.Heading + headingDeltaRadians * endFraction); + var middlePose = new Pose2D( + from.X + deltaX * middleFraction, + from.Y + deltaY * middleFraction, + from.Heading + headingDeltaRadians * middleFraction); + double segmentDeltaX = currentPose.X - previousPose.X; + double segmentDeltaY = currentPose.Y - previousPose.Y; + double segmentCenterDisplacementMeters = Math.Sqrt(segmentDeltaX * segmentDeltaX + segmentDeltaY * segmentDeltaY); + double segmentHeadingDeltaRadians = currentPose.Heading - previousPose.Heading; + double temporaryMarginMeters = 0.5d * (segmentCenterDisplacementMeters + + bodyFootprint.CircumscribedRadiusMeters * Math.Abs(segmentHeadingDeltaRadians)); + if (!NumericGuard.IsFinite(temporaryMarginMeters) || + !IsPoseCollisionFree(middlePose, map, vehicle, temporaryMarginMeters, out double middleClearanceMeters)) + return false; + minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, middleClearanceMeters); + previousPose = currentPose; + } + + if (!IsPoseCollisionFree(to, map, vehicle, 0d, out double toClearanceMeters)) return false; + minimumBodyClearanceMeters = Math.Min(minimumBodyClearanceMeters, toClearanceMeters); + return true; + } + + private static bool AreCornersInsideMap(VehicleFootprint footprint, PlanningGridMap map) + { + for (int index = 0; index < 4; index++) + { + footprint.GetCorner(index, out double cornerX, out double cornerY); + if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; + } + return true; + } + + private static double GetBodyClearance(double centerDistanceMeters, double bodyRadiusMeters) + { + if (double.IsPositiveInfinity(centerDistanceMeters)) return double.PositiveInfinity; + if (!NumericGuard.IsFinite(centerDistanceMeters) || !NumericGuard.IsFinite(bodyRadiusMeters)) return 0d; + return Math.Max(0d, centerDistanceMeters - bodyRadiusMeters); + } + + private static bool IntersectsOccupiedCell(VehicleFootprint footprint, PlanningGridMap map) + { + GetCellRange(map, footprint.MinX, footprint.MaxX, footprint.MinY, footprint.MaxY, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + if (!map.IsOccupied(row, col)) continue; + GetCellBoundsMeters(map, row, col, out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY); + if (OrientedRectangleCellIntersection.Intersects(footprint, cellMinX, cellMaxX, cellMinY, cellMaxY)) return true; + } + return false; + } + + private static void GetCellRange(PlanningGridMap map, double minX, double maxX, double minY, double maxY, + out int firstRow, out int lastRow, out int firstCol, out int lastCol) + { + double minimumMapX = map.Bounds.XMin / 1000d; + double minimumMapY = map.Bounds.YMin / 1000d; + firstCol = Clamp((int)Math.Floor((minX - minimumMapX) / map.ResolutionMeters) - 1, 0, map.Cols - 1); + lastCol = Clamp((int)Math.Floor((maxX - minimumMapX) / map.ResolutionMeters), 0, map.Cols - 1); + firstRow = Clamp((int)Math.Floor((minY - minimumMapY) / map.ResolutionMeters) - 1, 0, map.Rows - 1); + lastRow = Clamp((int)Math.Floor((maxY - minimumMapY) / map.ResolutionMeters), 0, map.Rows - 1); + } + + private static void GetCellBoundsMeters(PlanningGridMap map, int row, int col, + out double cellMinX, out double cellMaxX, out double cellMinY, out double cellMaxY) + { + cellMinX = map.Bounds.XMin / 1000d + col * map.ResolutionMeters; + cellMinY = map.Bounds.YMin / 1000d + row * map.ResolutionMeters; + cellMaxX = Math.Min(map.Bounds.XMax / 1000d, cellMinX + map.ResolutionMeters); + cellMaxY = Math.Min(map.Bounds.YMax / 1000d, cellMinY + map.ResolutionMeters); + } + + private static int Clamp(int value, int minimum, int maximum) + { + return value < minimum ? minimum : value > maximum ? maximum : value; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs new file mode 100644 index 0000000..53234be --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs @@ -0,0 +1,33 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// 旋转矩形与轴对齐栅格的分离轴相交判定。 +internal static class OrientedRectangleCellIntersection +{ + /// 任一投影轴没有严格分离时返回 true;擦边按相交处理。 + public static bool Intersects(VehicleFootprint rectangle, double cellMinX, double cellMaxX, double cellMinY, double cellMaxY) + { + if (rectangle == null || cellMaxX < cellMinX || cellMaxY < cellMinY) return false; + double cellCenterX = (cellMinX + cellMaxX) / 2d; + double cellCenterY = (cellMinY + cellMaxY) / 2d; + double cellHalfX = (cellMaxX - cellMinX) / 2d; + double cellHalfY = (cellMaxY - cellMinY) / 2d; + + return !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLongitudinalX, rectangle.AxisLongitudinalY) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, rectangle.AxisLateralX, rectangle.AxisLateralY) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 1d, 0d) && + !HasStrictSeparation(rectangle, cellCenterX, cellCenterY, cellHalfX, cellHalfY, 0d, 1d); + } + + private static bool HasStrictSeparation(VehicleFootprint rectangle, double cellCenterX, double cellCenterY, double cellHalfX, double cellHalfY, + double axisX, double axisY) + { + double rectangleCenter = rectangle.CenterX * axisX + rectangle.CenterY * axisY; + double cellCenter = cellCenterX * axisX + cellCenterY * axisY; + double rectangleRadius = rectangle.HalfLengthMeters * Math.Abs(rectangle.AxisLongitudinalX * axisX + rectangle.AxisLongitudinalY * axisY) + + rectangle.HalfWidthMeters * Math.Abs(rectangle.AxisLateralX * axisX + rectangle.AxisLateralY * axisY); + double cellRadius = cellHalfX * Math.Abs(axisX) + cellHalfY * Math.Abs(axisY); + return Math.Abs(rectangleCenter - cellCenter) > rectangleRadius + cellRadius; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs new file mode 100644 index 0000000..aa88e55 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs @@ -0,0 +1,90 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// 以车辆几何中心为原点的扩大旋转矩形。 +internal sealed class VehicleFootprint +{ + private VehicleFootprint(Pose2D pose, double halfLengthMeters, double halfWidthMeters) + { + CenterX = pose.X; + CenterY = pose.Y; + HalfLengthMeters = halfLengthMeters; + HalfWidthMeters = halfWidthMeters; + AxisLongitudinalX = Math.Cos(pose.Heading); + AxisLongitudinalY = Math.Sin(pose.Heading); + AxisLateralX = -AxisLongitudinalY; + AxisLateralY = AxisLongitudinalX; + CircumscribedRadiusMeters = Math.Sqrt(halfLengthMeters * halfLengthMeters + halfWidthMeters * halfWidthMeters); + + double minX = double.PositiveInfinity; + double maxX = double.NegativeInfinity; + double minY = double.PositiveInfinity; + double maxY = double.NegativeInfinity; + for (int index = 0; index < 4; index++) + { + GetCorner(index, out double x, out double y); + minX = Math.Min(minX, x); + maxX = Math.Max(maxX, x); + minY = Math.Min(minY, y); + maxY = Math.Max(maxY, y); + } + MinX = minX; + MaxX = maxX; + MinY = minY; + MaxY = maxY; + } + + public double CenterX { get; } + public double CenterY { get; } + public double HalfLengthMeters { get; } + public double HalfWidthMeters { get; } + public double AxisLongitudinalX { get; } + public double AxisLongitudinalY { get; } + public double AxisLateralX { get; } + public double AxisLateralY { get; } + public double CircumscribedRadiusMeters { get; } + public double MinX { get; } + public double MaxX { get; } + public double MinY { get; } + public double MaxY { get; } + + /// 创建包含车辆安全余量和临时扫掠余量的矩形。 + public static bool TryCreate(Pose2D pose, VehicleParameters vehicle, double additionalMarginMeters, out VehicleFootprint footprint) + { + footprint = null; + if (pose == null || vehicle == null || !NumericGuard.IsFinite(pose.X) || !NumericGuard.IsFinite(pose.Y) || + !NumericGuard.IsFinite(pose.Heading) || !NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || + !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || + vehicle.SafetyMarginMeters < 0d || !NumericGuard.IsFinite(additionalMarginMeters) || additionalMarginMeters < 0d) + return false; + + double totalMarginMeters = vehicle.SafetyMarginMeters + additionalMarginMeters; + if (!NumericGuard.IsFinite(totalMarginMeters)) return false; + double halfLengthMeters = vehicle.LengthMeters / 2d + totalMarginMeters; + double halfWidthMeters = vehicle.WidthMeters / 2d + totalMarginMeters; + if (!NumericGuard.IsPositiveFinite(halfLengthMeters) || !NumericGuard.IsPositiveFinite(halfWidthMeters)) return false; + + footprint = new VehicleFootprint(pose, halfLengthMeters, halfWidthMeters); + return NumericGuard.IsFinite(footprint.CircumscribedRadiusMeters) && NumericGuard.IsFinite(footprint.MinX) && + NumericGuard.IsFinite(footprint.MaxX) && NumericGuard.IsFinite(footprint.MinY) && NumericGuard.IsFinite(footprint.MaxY); + } + + /// 获取指定角点。索引按逆时针顺序为 0 到 3。 + public void GetCorner(int index, out double x, out double y) + { + double longitudinalSign; + double lateralSign; + switch (index) + { + case 0: longitudinalSign = 1d; lateralSign = 1d; break; + case 1: longitudinalSign = -1d; lateralSign = 1d; break; + case 2: longitudinalSign = -1d; lateralSign = -1d; break; + case 3: longitudinalSign = 1d; lateralSign = -1d; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + x = CenterX + longitudinalSign * HalfLengthMeters * AxisLongitudinalX + lateralSign * HalfWidthMeters * AxisLateralX; + y = CenterY + longitudinalSign * HalfLengthMeters * AxisLongitudinalY + lateralSign * HalfWidthMeters * AxisLateralY; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs new file mode 100644 index 0000000..88bed55 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs @@ -0,0 +1,40 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; + +/// +/// 从车辆参数提取运动学限制的辅助方法。 +/// 曲率单位为 1/m;当最大曲率与最小转弯半径同时给出时,始终选择更保守的较小曲率。 +/// +public static class VehicleKinematics +{ + /// + /// 尝试获取车辆允许的最大绝对曲率。 + /// 参数:vehicle 为车辆参数;maximumCurvaturePerMeter 为输出的正有限曲率,单位 1/m。 + /// 返回:至少提供一种正有限曲率限制时为 true;任一已提供限制无效或两种限制均未提供时为 false。 + /// + public static bool TryGetMaximumCurvaturePerMeter(VehicleParameters vehicle, out double maximumCurvaturePerMeter) + { + maximumCurvaturePerMeter = 0d; + if (vehicle == null) return false; + + bool hasMaximumCurvature = vehicle.MaximumCurvaturePerMeter.HasValue; + bool hasMinimumRadius = vehicle.MinimumTurningRadiusMeters.HasValue; + if (hasMaximumCurvature && !NumericGuard.IsPositiveFinite(vehicle.MaximumCurvaturePerMeter.Value)) return false; + if (hasMinimumRadius && !NumericGuard.IsPositiveFinite(vehicle.MinimumTurningRadiusMeters.Value)) return false; + if (!hasMaximumCurvature && !hasMinimumRadius) return false; + + if (hasMaximumCurvature && hasMinimumRadius) + { + maximumCurvaturePerMeter = System.Math.Min( + vehicle.MaximumCurvaturePerMeter.Value, + 1d / vehicle.MinimumTurningRadiusMeters.Value); + return true; + } + + maximumCurvaturePerMeter = hasMaximumCurvature + ? vehicle.MaximumCurvaturePerMeter.Value + : 1d / vehicle.MinimumTurningRadiusMeters.Value; + return NumericGuard.IsPositiveFinite(maximumCurvaturePerMeter); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html b/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html new file mode 100644 index 0000000..6269f51 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Coras_Planner/Designer_corasplanner/AMR_HybridAStar_粗路径规划总体技术方案.html @@ -0,0 +1,812 @@ + + + + + +AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案 + + + +
+

AMR 非结构化道路 Hybrid A* 粗路径规划总体技术方案

+

+ 面向四舵轮AMR的第一阶段工程实现:暂不考虑蟹行与纯横移,仅采用车式运动模型, + 支持前进、倒车及换向,输出供后续SQP使用的无时间空间粗路径。 + 本文统一整理已经确认的第1至第14步,作为软件设计、编码、调试与验收依据。 +

+
+ 步骤1–12:已确认 + 步骤13:性能优化TODO + 步骤14:接口与验收已确认 + 地图分辨率初值:0.05 m + 原语长度:0.50 m + 积分步长:0.05 m +
+
+ +
+ +
+ 文档用途:四舵轮AMR第一阶段非结构化道路粗路径规划开发依据。内部单位统一采用米、弧度、1/米。 +
+ + diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs index 6bcbb34..5840009 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs @@ -1,4 +1,4 @@ -namespace MultiWheelC.TrajectoryPlanning.EMPlanner; +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; public sealed partial class EmPlannerConfiguration { @@ -17,8 +17,8 @@ public sealed partial class EmPlannerConfiguration Scheduling = new SchedulingConfiguration { ReplanPeriodSeconds = 0.20d, - TimeHorizonSeconds = 6d, - DistanceHorizonMeters = 5d, + TimeHorizonSeconds = 6d, //窗口的规划时间长度。单位s + DistanceHorizonMeters = 500d, //最大探索s里程距离,单位m OutputTimeStepSeconds = 0.05d, SolverTimeoutSeconds = 0.10d, HandoffLookaheadSeconds = 0.30d, diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs index 3527637..e574c83 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; @@ -43,11 +44,19 @@ public sealed class EmPlanningService : IEmPlanningService EmitDebug(request, "direction-segment selection succeeded"); var projector = new FrenetProjector(); - if (!projector.TryProject(request.VehicleState.Pose, segment, 0d, segment.LengthMeters, + double startProjectionUpperS = request.PlanningScope == EmPlanningScope.FullDirectionSegment + ? Math.Min(segment.LengthMeters, configuration.Frenet.MaximumProjectionDistanceMeters) + : segment.LengthMeters; + if (!projector.TryProject(request.VehicleState.Pose, segment, 0d, startProjectionUpperS, configuration.Frenet.MaximumProjectionDistanceMeters, 0d, out FrenetProjection startProjection)) { return Failure(EmPlanningStatus.ProjectionFailed, request, - "Vehicle pose could not be projected inside the selected direction segment."); + "Vehicle pose could not be projected at an admissible start of the selected direction segment."); + } + if (Math.Abs(startProjection.HeadingError) >= Math.PI / 2d) + { + return Failure(EmPlanningStatus.ProjectionFailed, request, + "Vehicle travel heading differs by at least 90 degrees from the selected direction segment."); } EmitDebug(request, "bounded ego projection succeeded"); @@ -78,9 +87,13 @@ public sealed class EmPlanningService : IEmPlanningService var lateralInput = new LateralPlanningInput(segment, corridor, startProjection, horizon.TerminalType, request.Vehicle, configuration, previousSeed); + TimeSpan totalSolveBudget = TimeSpan.FromSeconds(configuration.Scheduling.SolverTimeoutSeconds); + var solveBudgetStopwatch = Stopwatch.StartNew(); LateralPlanningResult lateral = new LateralPlanner(qpSolver).Plan(lateralInput, cancellationToken); if (!IsSuccess(lateral.Status)) return Failure(lateral.Status, request, lateral.FailureReason); + if (cancellationToken.IsCancellationRequested) + return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after LS optimization."); EmitDebug(request, "LS optimization and validation succeeded"); EmPlanningStatus envelopeStatus = new PathSpeedLimitBuilder().Build(lateral.Path, segment.Direction, @@ -106,8 +119,16 @@ public sealed class EmPlanningService : IEmPlanningService new LongitudinalPreviousTrajectorySeedBuilder().Build( request.PreviousTrajectory, lateral.Path, request.EffectiveAtUtc, knotSchedule, segment.SegmentIndex, segment.Direction); + TimeSpan remainingSolveBudget = totalSolveBudget - solveBudgetStopwatch.Elapsed; + if (remainingSolveBudget <= TimeSpan.Zero) + { + return Failure(EmPlanningStatus.SolverTimedOut, request, + "LS/ST optimization exhausted the shared solve budget before ST optimization."); + } + EmPlannerConfiguration longitudinalConfiguration = configuration.Copy(); + longitudinalConfiguration.Scheduling.SolverTimeoutSeconds = remainingSolveBudget.TotalSeconds; var longitudinalInput = new LongitudinalPlanningInput(lateral.Path, segment.Direction, initialProgressSpeed, - initialAcceleration, horizon.TerminalType, horizon.LongitudinalMode, configuration, + initialAcceleration, horizon.TerminalType, horizon.LongitudinalMode, longitudinalConfiguration, request.PlanningScope, knotSchedule, previousLongitudinalSeed.PathS, previousLongitudinalSeed.ProgressSpeedMetersPerSecond); envelopeStatus = new PathSpeedLimitBuilder().Build(longitudinalInput, out _, out envelopeReason); @@ -118,6 +139,8 @@ public sealed class EmPlanningService : IEmPlanningService LongitudinalPlanningResult longitudinal = new LongitudinalPlanner(qpSolver).Plan(longitudinalInput, cancellationToken); if (!IsSuccess(longitudinal.Status)) return Failure(longitudinal.Status, request, longitudinal.FailureReason); + if (cancellationToken.IsCancellationRequested) + return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after ST optimization."); EmitDebug(request, "ST optimization and validation succeeded"); var metadata = new EmTrajectoryMetadata(request.OutputTrajectoryId, request.RequestedAtUtc, request.EffectiveAtUtc, @@ -128,6 +151,8 @@ public sealed class EmPlanningService : IEmPlanningService longitudinal, metadata, out EmTrajectory trajectory, out string assemblyFailure); if (assemblyStatus != EmPlanningStatus.Success) return Failure(assemblyStatus, request, assemblyFailure); + if (cancellationToken.IsCancellationRequested) + return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled after trajectory assembly."); EmitDebug(request, "trajectory assembly succeeded"); EmBoundaryType terminalBoundary = slice.TerminalBoundary.BoundaryType; @@ -135,7 +160,7 @@ public sealed class EmPlanningService : IEmPlanningService ? TerminalPose(lateral.Path) : null; EmTrajectoryValidationResult publication = new EmTrajectoryValidator().Validate(trajectory, request.Map, - request.Vehicle, configuration, segment.SegmentIndex, longitudinalInput.PathUpperBoundS, + request.Vehicle, configuration, segment.SegmentIndex, segment.LengthMeters, longitudinalInput.PathUpperBoundS, terminalPose, terminalBoundary); if (!publication.IsValid) { @@ -144,6 +169,9 @@ public sealed class EmPlanningService : IEmPlanningService } EmitDebug(request, "world-space publication validation succeeded"); + if (cancellationToken.IsCancellationRequested) + return Failure(EmPlanningStatus.Cancelled, request, "Planning was cancelled before trajectory publication."); + EmPlanningStatus finalStatus = lateral.Status == EmPlanningStatus.SuccessWithFallback || longitudinal.Status == EmPlanningStatus.SuccessWithFallback ? EmPlanningStatus.SuccessWithFallback diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs index 2b5aef7..2d2e7d8 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs @@ -35,7 +35,7 @@ public sealed class LateralConstraintBuilder _objectiveBuilder.AddTerms(input, layout, linearization, hessian, linearCost); int terminalRows = input.TerminalType == EmTerminalType.RollingSafetyStop ? 0 : 2; - var constraints = new SparseTripletBuilder(7 * layout.StationCount - 2 + terminalRows, layout.VariableCount); + var constraints = new SparseTripletBuilder(8 * layout.StationCount - 2 + terminalRows, layout.VariableCount); var lower = new List(); var upper = new List(); int row = 0; @@ -43,12 +43,13 @@ public sealed class LateralConstraintBuilder if (!TryAddLateralBounds(input, layout, linearization, constraints, lower, upper, ref row, out failureReason)) return false; AddDerivativeBounds(input, layout, constraints, lower, upper, ref row); + AddCurvatureBounds(input, layout, linearization, constraints, lower, upper, ref row); AddStartConstraints(input, layout, constraints, lower, upper, ref row); AddExactDynamics(input.ReferenceStations, layout, constraints, lower, upper, ref row); if (input.TerminalType != EmTerminalType.RollingSafetyStop) AddTerminalConstraints(layout, constraints, lower, upper, ref row); - if (row != 7 * layout.StationCount - 2 + terminalRows) + if (row != 8 * layout.StationCount - 2 + terminalRows) throw new InvalidOperationException("Lateral constraint row accounting is inconsistent."); problem = new QuadraticProgram(hessian.Build(), linearCost, constraints.Build(), lower, upper); return true; @@ -131,6 +132,21 @@ public sealed class LateralConstraintBuilder AddSingleVariableRow(constraints, lower, upper, ref row, layout.DDDL(interval), -third, third); } + private static void AddCurvatureBounds(LateralPlanningInput input, LateralVariableLayout layout, + LateralCandidate linearization, SparseTripletBuilder constraints, IList lower, IList upper, + ref int row) + { + double maximumCurvature = LateralCurvatureLinearization.GetMaximumVehicleCurvature(input.Vehicle); + IReadOnlyList affines = LateralCurvatureLinearization.Create(input, layout, + linearization); + for (int station = 0; station < affines.Count; station++) + { + LateralCurvatureLinearization affine = affines[station]; + AddRow(constraints, lower, upper, ref row, -maximumCurvature - affine.Constant, + maximumCurvature - affine.Constant, affine.VariableIndices, affine.Gradient); + } + } + private static void AddStartConstraints(LateralPlanningInput input, LateralVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs new file mode 100644 index 0000000..f5a8581 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +/// Shared affine vehicle-curvature model used by the LS objective and hard QP constraints. +internal sealed class LateralCurvatureLinearization +{ + private LateralCurvatureLinearization(int[] variableIndices, double[] gradient, double constant) + { + VariableIndices = variableIndices; + Gradient = gradient; + Constant = constant; + } + + internal IReadOnlyList VariableIndices { get; } + + internal IReadOnlyList Gradient { get; } + + internal double Constant { get; } + + internal static IReadOnlyList Create(LateralPlanningInput input, + LateralVariableLayout layout, LateralCandidate linearization) + { + if (input == null) throw new ArgumentNullException(nameof(input)); + if (layout == null) throw new ArgumentNullException(nameof(layout)); + if (linearization == null) throw new ArgumentNullException(nameof(linearization)); + if (layout.StationCount != input.ReferenceStations.Count || + linearization.ReferenceStations.Count != layout.StationCount) + { + throw new ArgumentException("Curvature linearization stations must match the lateral layout.", nameof(linearization)); + } + + double directionSign = input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d; + var affines = new List(layout.StationCount); + for (int station = 0; station < layout.StationCount; station++) + { + FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, + input.ReferenceStations[station]); + double l = linearization.L[station]; + double dl = linearization.DL[station]; + double ddl = linearization.DDL[station]; + double referenceCurvature = reference.GeometricCurvature; + double referenceCurvatureDerivative = directionSign * reference.VehicleCurvatureDerivative; + double a = 1d - referenceCurvature * l; + double denominatorSquared = a * a + dl * dl; + if (!IsFinite(denominatorSquared) || denominatorSquared <= 0d) + throw new ArgumentException("Curvature linearization denominator is invalid.", nameof(linearization)); + + double denominatorPow3Over2 = denominatorSquared * Math.Sqrt(denominatorSquared); + double denominatorPow5Over2 = denominatorPow3Over2 * denominatorSquared; + double numerator = a * a * referenceCurvature + a * ddl + + referenceCurvatureDerivative * l * dl + 2d * referenceCurvature * dl * dl; + double geometricCurvature = numerator / denominatorPow3Over2; + double dNumeratorDLateral = -2d * a * referenceCurvature * referenceCurvature - + referenceCurvature * ddl + referenceCurvatureDerivative * dl; + double dNumeratorDSlope = referenceCurvatureDerivative * l + 4d * referenceCurvature * dl; + double dDenominatorSquaredDLateral = -2d * a * referenceCurvature; + double dDenominatorSquaredDSlope = 2d * dl; + double dGeometricDLateral = dNumeratorDLateral / denominatorPow3Over2 - + 1.5d * numerator * dDenominatorSquaredDLateral / denominatorPow5Over2; + double dGeometricDSlope = dNumeratorDSlope / denominatorPow3Over2 - + 1.5d * numerator * dDenominatorSquaredDSlope / denominatorPow5Over2; + double dGeometricDSecondDerivative = a / denominatorPow3Over2; + double vehicleCurvature = directionSign * geometricCurvature; + double[] gradient = + { + directionSign * dGeometricDLateral, + directionSign * dGeometricDSlope, + directionSign * dGeometricDSecondDerivative, + }; + double constant = vehicleCurvature - gradient[0] * l - gradient[1] * dl - gradient[2] * ddl; + if (!IsFinite(vehicleCurvature) || !IsFinite(constant) || !IsFinite(gradient[0]) || + !IsFinite(gradient[1]) || !IsFinite(gradient[2])) + { + throw new ArgumentException("Curvature linearization is non-finite.", nameof(linearization)); + } + affines.Add(new LateralCurvatureLinearization( + new[] { layout.L(station), layout.DL(station), layout.DDL(station) }, gradient, constant)); + } + return affines; + } + + internal static double GetMaximumVehicleCurvature(VehicleParameters vehicle) + { + if (vehicle == null) throw new ArgumentNullException(nameof(vehicle)); + double maximum = vehicle.MaximumCurvaturePerMeter ?? + (vehicle.MinimumTurningRadiusMeters.HasValue && vehicle.MinimumTurningRadiusMeters.Value > 0d + ? 1d / vehicle.MinimumTurningRadiusMeters.Value + : double.NaN); + if (!IsFinite(maximum) || maximum <= 0d) + throw new ArgumentException("Vehicle maximum curvature is required.", nameof(vehicle)); + return maximum; + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs index 5f0c576..b52ade8 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs @@ -27,7 +27,8 @@ public sealed class LateralObjectiveBuilder double slopeScale = RequirePositive(lateral.MaximumLateralSlope, "slope scale"); double secondDerivativeScale = RequirePositive(lateral.MaximumLateralSecondDerivativePerMeter, "second-derivative scale"); double thirdDerivativeScale = RequirePositive(lateral.MaximumLateralThirdDerivativePerSquareMeter, "third-derivative scale"); - double curvatureScale = RequirePositive(GetMaximumVehicleCurvature(input.Vehicle), "curvature scale"); + double curvatureScale = RequirePositive(LateralCurvatureLinearization.GetMaximumVehicleCurvature(input.Vehicle), + "curvature scale"); double curvatureVariationScale = GetCurvatureVariationScale(input); for (int station = 0; station < layout.StationCount; station++) @@ -46,10 +47,11 @@ public sealed class LateralObjectiveBuilder } AddPreviousTrajectoryTerms(input, layout, hessian, linearCost, weights.PreviousTrajectory, lateralScale); - CurvatureAffine[] curvature = CreateCurvatureAffines(input, layout, linearization); - for (int station = 0; station < curvature.Length; station++) + IReadOnlyList curvature = LateralCurvatureLinearization.Create(input, layout, + linearization); + for (int station = 0; station < curvature.Count; station++) { - AddSquaredResidual(hessian, linearCost, curvature[station].Indices, curvature[station].Gradient, + AddSquaredResidual(hessian, linearCost, curvature[station].VariableIndices, curvature[station].Gradient, curvature[station].Constant, weights.Curvature, curvatureScale); } AddCurvatureVariationTerms(input.ReferenceStations, curvature, hessian, linearCost, weights.CurvatureVariation, @@ -75,79 +77,27 @@ public sealed class LateralObjectiveBuilder } } - private static CurvatureAffine[] CreateCurvatureAffines(LateralPlanningInput input, LateralVariableLayout layout, - LateralCandidate linearization) - { - var affines = new CurvatureAffine[layout.StationCount]; - double directionSign = input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d; - for (int station = 0; station < layout.StationCount; station++) - { - FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, - input.ReferenceStations[station]); - double l = linearization.L[station]; - double dl = linearization.DL[station]; - double ddl = linearization.DDL[station]; - double referenceCurvature = reference.GeometricCurvature; - double referenceCurvatureDerivative = directionSign * reference.VehicleCurvatureDerivative; - double a = 1d - referenceCurvature * l; - double denominatorSquared = a * a + dl * dl; - if (!IsFinite(denominatorSquared) || denominatorSquared <= 0d) - throw new ArgumentException("Curvature linearization denominator is invalid.", nameof(linearization)); - - double denominatorPow3Over2 = denominatorSquared * Math.Sqrt(denominatorSquared); - double denominatorPow5Over2 = denominatorPow3Over2 * denominatorSquared; - double numerator = a * a * referenceCurvature + a * ddl + - referenceCurvatureDerivative * l * dl + 2d * referenceCurvature * dl * dl; - double geometricCurvature = numerator / denominatorPow3Over2; - double dNumeratorDLateral = -2d * a * referenceCurvature * referenceCurvature - - referenceCurvature * ddl + referenceCurvatureDerivative * dl; - double dNumeratorDSlope = referenceCurvatureDerivative * l + 4d * referenceCurvature * dl; - double dDenominatorSquaredDLateral = -2d * a * referenceCurvature; - double dDenominatorSquaredDSlope = 2d * dl; - double dGeometricDLateral = dNumeratorDLateral / denominatorPow3Over2 - - 1.5d * numerator * dDenominatorSquaredDLateral / denominatorPow5Over2; - double dGeometricDSlope = dNumeratorDSlope / denominatorPow3Over2 - - 1.5d * numerator * dDenominatorSquaredDSlope / denominatorPow5Over2; - double dGeometricDSecondDerivative = a / denominatorPow3Over2; - double vehicleCurvature = directionSign * geometricCurvature; - double[] gradient = - { - directionSign * dGeometricDLateral, - directionSign * dGeometricDSlope, - directionSign * dGeometricDSecondDerivative, - }; - double constant = vehicleCurvature - gradient[0] * l - gradient[1] * dl - gradient[2] * ddl; - if (!IsFinite(vehicleCurvature) || !IsFinite(constant) || !IsFinite(gradient[0]) || - !IsFinite(gradient[1]) || !IsFinite(gradient[2])) - { - throw new ArgumentException("Curvature linearization is non-finite.", nameof(linearization)); - } - affines[station] = new CurvatureAffine(new[] { layout.L(station), layout.DL(station), layout.DDL(station) }, - gradient, constant); - } - return affines; - } - - private static void AddCurvatureVariationTerms(IReadOnlyList stations, CurvatureAffine[] curvature, + private static void AddCurvatureVariationTerms(IReadOnlyList stations, + IReadOnlyList curvature, SparseTripletBuilder hessian, IList linearCost, double weight, double scale) { - for (int station = 0; station < curvature.Length; station++) + for (int station = 0; station < curvature.Count; station++) { int lower = station == 0 ? 0 : station - 1; - int upper = station == curvature.Length - 1 ? curvature.Length - 1 : station + 1; + int upper = station == curvature.Count - 1 ? curvature.Count - 1 : station + 1; double ds = stations[upper] - stations[lower]; if (!IsFinite(ds) || ds <= 0d) throw new ArgumentException("Curvature variation requires strictly increasing stations.", nameof(stations)); - CurvatureAffine left = curvature[lower]; - CurvatureAffine right = curvature[upper]; - var indices = new int[left.Indices.Length + right.Indices.Length]; + LateralCurvatureLinearization left = curvature[lower]; + LateralCurvatureLinearization right = curvature[upper]; + var indices = new int[left.VariableIndices.Count + right.VariableIndices.Count]; var gradient = new double[indices.Length]; - for (int index = 0; index < left.Indices.Length; index++) + for (int index = 0; index < left.VariableIndices.Count; index++) { - indices[index] = left.Indices[index]; + indices[index] = left.VariableIndices[index]; gradient[index] = -left.Gradient[index] / ds; - indices[left.Indices.Length + index] = right.Indices[index]; - gradient[left.Indices.Length + index] = right.Gradient[index] / ds; + indices[left.VariableIndices.Count + index] = right.VariableIndices[index]; + gradient[left.VariableIndices.Count + index] = right.Gradient[index] / ds; } AddSquaredResidual(hessian, linearCost, indices, gradient, (right.Constant - left.Constant) / ds, weight, scale); } @@ -209,17 +159,6 @@ public sealed class LateralObjectiveBuilder return Math.Max(1d, maximum); } - private static double GetMaximumVehicleCurvature(VehicleParameters vehicle) - { - if (vehicle == null) - throw new ArgumentNullException(nameof(vehicle)); - if (vehicle.MaximumCurvaturePerMeter.HasValue) - return vehicle.MaximumCurvaturePerMeter.Value; - if (vehicle.MinimumTurningRadiusMeters.HasValue && vehicle.MinimumTurningRadiusMeters.Value > 0d) - return 1d / vehicle.MinimumTurningRadiusMeters.Value; - throw new ArgumentException("Vehicle maximum curvature is required.", nameof(vehicle)); - } - private static double RequirePositive(double value, string name) { if (!IsFinite(value) || value <= 0d) @@ -232,17 +171,4 @@ public sealed class LateralObjectiveBuilder return !double.IsNaN(value) && !double.IsInfinity(value); } - private sealed class CurvatureAffine - { - public CurvatureAffine(int[] indices, double[] gradient, double constant) - { - Indices = indices; - Gradient = gradient; - Constant = constant; - } - - public int[] Indices { get; } - public double[] Gradient { get; } - public double Constant { get; } - } } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs index 846aec1..eb68f70 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs @@ -68,6 +68,8 @@ public sealed class SequentialConvexOptimizer remainingBudget, settings.EnableWarmStart, settings.EnablePolishing, settings.EnableNativeVerboseOutput), warmStart, cancellationToken); + if (cancellationToken.IsCancellationRequested) + return Failed(EmPlanningStatus.Cancelled, "Lateral SQP was cancelled after the QP solve."); if (solved == null) return FallbackOrFailure(lastValidatedPath, EmPlanningStatus.Failed, "The lateral QP solver returned no result."); @@ -100,16 +102,27 @@ public sealed class SequentialConvexOptimizer continue; if (!_geometryEvaluator.TryEvaluate(input, candidate, out LateralPath evaluatedPath, out _)) continue; + + // A geometrically evaluable QP solution that remains inside the static + // corridor is the next SQP linearization point, even when the independent + // validator rejects it. Otherwise the next outer pass rebuilds the identical + // convex subproblem and cannot correct that result. An out-of-corridor vector + // must not become an iterate: it can make the following trust region infeasible. + LateralCandidate previousIterate = iterate; + if (RespectsStaticCorridor(input, candidate)) + { + iterate = candidate; + warmStart = CopyValues(solved.Primal); + } + if (!_solutionValidator.TryValidate(input, candidate, evaluatedPath, out LateralPath validatedPath, out _)) continue; - double maximumLateralChange = MaximumLateralChange(iterate, candidate); + double maximumLateralChange = MaximumLateralChange(previousIterate, candidate); double relativeObjectiveImprovement = hasPreviousObjective ? RelativeObjectiveImprovement(previousObjective, solved.Objective) : double.PositiveInfinity; lastValidatedPath = CopyPath(validatedPath); - iterate = candidate; - warmStart = CopyValues(solved.Primal); previousObjective = solved.Objective; hasPreviousObjective = true; @@ -119,7 +132,10 @@ public sealed class SequentialConvexOptimizer return lastValidatedPath == null ? Failed(EmPlanningStatus.LateralInfeasible, "No independently validated lateral candidate was found.") - : new LateralPlanningResult(EmPlanningStatus.Success, lastValidatedPath, string.Empty); + : new LateralPlanningResult( + EmPlanningStatus.SuccessWithFallback, + lastValidatedPath, + "Lateral SQP reached its outer-iteration limit before strict convergence."); } private static bool TryCreateSettings(LateralPlanningInput input, out QpSolverSettings settings, out TimeSpan totalBudget, @@ -226,6 +242,17 @@ public sealed class SequentialConvexOptimizer return maximum; } + private static bool RespectsStaticCorridor(LateralPlanningInput input, LateralCandidate candidate) + { + for (int index = 0; index < candidate.L.Count; index++) + { + LateralInterval interval = input.Corridor.Stations[index]; + if (candidate.L[index] < interval.MinimumL || candidate.L[index] > interval.MaximumL) + return false; + } + return true; + } + private static double RelativeObjectiveImprovement(double previous, double current) { return Math.Abs(previous - current) / Math.Max(1d, Math.Abs(previous)); @@ -233,7 +260,7 @@ public sealed class SequentialConvexOptimizer private static LateralPlanningResult FallbackOrFailure(LateralPath path, EmPlanningStatus failureStatus, string failureReason) { - return path == null + return failureStatus == EmPlanningStatus.Cancelled || path == null ? Failed(failureStatus, failureReason) : new LateralPlanningResult(EmPlanningStatus.SuccessWithFallback, path, failureReason); } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs index a2bb074..349dc8d 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs @@ -119,6 +119,8 @@ public sealed class SequentialLongitudinalOptimizer settings.EnablePolishing, settings.EnableNativeVerboseOutput), warmStart, cancellationToken); + if (cancellationToken.IsCancellationRequested) + return Failed(EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled after the QP solve."); if (solved == null) return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Failed, "The longitudinal QP solver returned no result."); if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations) @@ -293,6 +295,12 @@ public sealed class SequentialLongitudinalOptimizer settings.EnablePolishing, settings.EnableNativeVerboseOutput), ToPrimal(linearizationIterate), cancellationToken); projectionSolveCount++; + if (cancellationToken.IsCancellationRequested) + { + failureStatus = EmPlanningStatus.Cancelled; + failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection was cancelled after the QP solve."); + return false; + } if (solved == null) { failureStatus = EmPlanningStatus.Failed; @@ -1196,7 +1204,7 @@ public sealed class SequentialLongitudinalOptimizer private static LongitudinalPlanningResult FallbackOrFailure(LongitudinalCandidate candidate, EmPlanningStatus failureStatus, string failureReason) { - return candidate == null + return failureStatus == EmPlanningStatus.Cancelled || candidate == null ? Failed(failureStatus, failureReason) : new LongitudinalPlanningResult(EmPlanningStatus.SuccessWithFallback, candidate, failureReason); } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs index 8793677..ad63ba0 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs @@ -81,7 +81,7 @@ public sealed class EmTrajectoryAssembler EmBoundaryType boundaryType = isTerminalAnchor ? ToBoundaryType(metadata.TerminalType) : EmBoundaryType.None; double signedSpeed = directionSign * sample.ProgressSpeed; points.Add(new EmTrajectoryPoint(geometry.X, geometry.Y, geometry.Yaw, signedSpeed, sample.TimeFromStart, - geometry.VehicleCurvature, metadata.SegmentIndex, sample.PathS, sample.PathS, metadata.Direction, + geometry.VehicleCurvature, metadata.SegmentIndex, geometry.ReferenceS, sample.PathS, metadata.Direction, boundaryType, sample.Acceleration, sample.Jerk)); } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs index 44eed00..428dc55 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs @@ -46,6 +46,7 @@ internal sealed class LateralPathInterpolator LateralPathPoint left = path.Points[index - 1]; double ratio = (pathS - left.PathS) / (right.PathS - left.PathS); return new InterpolatedLateralPathPoint( + Linear(left.ReferenceS, right.ReferenceS, ratio), Linear(left.X, right.X, ratio), Linear(left.Y, right.Y, ratio), NormalizeYaw(left.VehicleYaw + ratio * NormalizeYaw(right.VehicleYaw - left.VehicleYaw)), @@ -58,7 +59,8 @@ internal sealed class LateralPathInterpolator private static InterpolatedLateralPathPoint From(LateralPathPoint point) { - return new InterpolatedLateralPathPoint(point.X, point.Y, NormalizeYaw(point.VehicleYaw), point.VehicleCurvature); + return new InterpolatedLateralPathPoint(point.ReferenceS, point.X, point.Y, NormalizeYaw(point.VehicleYaw), + point.VehicleCurvature); } private static double Linear(double left, double right, double ratio) @@ -84,14 +86,16 @@ internal sealed class LateralPathInterpolator internal sealed class InterpolatedLateralPathPoint { - public InterpolatedLateralPathPoint(double x, double y, double yaw, double vehicleCurvature) + public InterpolatedLateralPathPoint(double referenceS, double x, double y, double yaw, double vehicleCurvature) { + ReferenceS = referenceS; X = x; Y = y; Yaw = yaw; VehicleCurvature = vehicleCurvature; } + public double ReferenceS { get; } public double X { get; } public double Y { get; } public double Yaw { get; } diff --git a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs index 7e281e9..a8cad33 100644 --- a/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs @@ -81,7 +81,7 @@ public sealed class EmTrajectoryValidator public EmTrajectoryValidationResult Validate(EmTrajectory trajectory, PlanningGridMap map, VehicleParameters vehicle, EmPlannerConfiguration configuration, int segmentIndex, double pathUpperBoundS, EmBoundaryType terminalBoundary) { - return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, pathUpperBoundS, null, + return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, double.PositiveInfinity, pathUpperBoundS, null, terminalBoundary, false); } @@ -89,12 +89,34 @@ public sealed class EmTrajectoryValidator EmPlannerConfiguration configuration, int segmentIndex, double pathUpperBoundS, Pose2D terminalPose, EmBoundaryType terminalBoundary) { - return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, pathUpperBoundS, terminalPose, + return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, double.PositiveInfinity, pathUpperBoundS, terminalPose, terminalBoundary, true); } + public EmTrajectoryValidationResult Validate(EmTrajectory trajectory, PlanningGridMap map, VehicleParameters vehicle, + EmPlannerConfiguration configuration, int segmentIndex, double segmentUpperBoundS, double pathUpperBoundS, + EmBoundaryType terminalBoundary) + { + if (!IsFinite(segmentUpperBoundS) || segmentUpperBoundS < 0d) + return EmTrajectoryValidationResult.Reject(EmTrajectoryValidationFailure.InvalidInput, -1, + "Segment-local trajectory bounds are invalid."); + return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, segmentUpperBoundS, pathUpperBoundS, + null, terminalBoundary, false); + } + + public EmTrajectoryValidationResult Validate(EmTrajectory trajectory, PlanningGridMap map, VehicleParameters vehicle, + EmPlannerConfiguration configuration, int segmentIndex, double segmentUpperBoundS, double pathUpperBoundS, + Pose2D terminalPose, EmBoundaryType terminalBoundary) + { + if (!IsFinite(segmentUpperBoundS) || segmentUpperBoundS < 0d) + return EmTrajectoryValidationResult.Reject(EmTrajectoryValidationFailure.InvalidInput, -1, + "Segment-local trajectory bounds are invalid."); + return ValidateCore(trajectory, map, vehicle, configuration, segmentIndex, segmentUpperBoundS, pathUpperBoundS, + terminalPose, terminalBoundary, true); + } + private EmTrajectoryValidationResult ValidateCore(EmTrajectory trajectory, PlanningGridMap map, VehicleParameters vehicle, - EmPlannerConfiguration configuration, int segmentIndex, double pathUpperBoundS, Pose2D terminalPose, + EmPlannerConfiguration configuration, int segmentIndex, double segmentUpperBoundS, double pathUpperBoundS, Pose2D terminalPose, EmBoundaryType terminalBoundary, bool requiresTerminalPose) { if (trajectory == null || map == null || vehicle == null || configuration == null || configuration.Validation == null || @@ -111,7 +133,7 @@ public sealed class EmTrajectoryValidator EmTrajectoryPoint point = trajectory.Points[index]; if (point == null || !HasOnlyFiniteValues(point)) return Reject(EmTrajectoryValidationFailure.NonFinite, index, "Trajectory contains a non-finite point."); - if (point.SegmentIndex != segmentIndex || point.SegmentLocalS > pathUpperBoundS + limits.SpatialTolerance || + if (point.SegmentIndex != segmentIndex || point.SegmentLocalS > segmentUpperBoundS + limits.SpatialTolerance || point.PathS > pathUpperBoundS + limits.SpatialTolerance) { return Reject(EmTrajectoryValidationFailure.SegmentBoundaryExceeded, index, diff --git a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md index 12cb362..9ee4f13 100644 --- a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md +++ b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_HybridAStar_粗路径实施计划.md @@ -1,347 +1,726 @@ # Hybrid A* 粗路径规划实施计划 -> For agentic workers: implement task-by-task with checkbox tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 -**目标:** 完成可独立调用的规划地图入口与 Hybrid A* 粗路径输出。所有核心功能和自动化测试通过后,最后编写 MovementTest。 +**目标:** 在 ClumsyPilot `netstandard2.0` 项目中交付可复用的静态规划地图和 Hybrid A* 粗路径规划模块,通过一次 `CoarsePathPlanningService.Plan(job)` 完成建图、快照复用、粗路径搜索和可选调试发布。 -**架构:** Map 的唯一公共调用类是 PlanningMapFactory;CoarsePath 的唯一公共调用类是 HybridAStarPlanner。Map 输出只读 PlanningGridMap,Planner 只接收该输入。 +**架构:** `Map` 模块统一接收人工障碍、TwoLeg 及未来障碍来源,生成只含外部障碍物的不可变 `PlanningGridMap`;`HybridAStarPlanner` 只消费该快照并完成保守碰撞检查与搜索;`CoarsePathPlanningService` 负责编排。核心逻辑不读取传感器、UI 或系统时间,Clumsy `MovementTest` 和 PNG 导出只作为旁路适配器。 -**技术栈:** C# 10、.NET Standard 2.0、现有 Clumsy 引用、PowerShell 反射测试、Clumsy MovementTest。 +**技术栈:** C# 10、.NET Standard 2.0、PowerShell 反射契约测试、Clumsy `MovementTest`、StbImageWriteSharp 1.16.7。 + +**设计依据:** `docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md` ## 全局约束 -- 外部地图与障碍物使用现有世界坐标 mm;规划内部使用 m、rad、1/m。 -- 环境占据图只保存外部障碍物;车体尺寸与安全余量只属于碰撞检查。 -- 支持中心放置的轴对齐矩形、圆形和可选 TwoLegDetect 投影;障碍物不膨胀。 -- 第一版终点使用可配置位置/航向容差;不含 Reeds-Shepp、平滑、SQP、时间轨迹与控制。 -- 每个公共接口必须有成功、边界、失败测试;MovementTest 是最后一项任务。 +- 所有命令均从仓库根目录执行;项目文件为 `ClumsyPilot/ClumsyPilot.csproj`。 +- 目标框架保持 `netstandard2.0`,不得直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite` 或依赖 `record/init` 的实现。 +- 地图边界、人工障碍和 TwoLeg 快照使用世界坐标 mm;规划内部统一使用 m、rad、1/m。 +- 地图只保存外部障碍物,不写入 AMR 自身足迹,不在地图侧添加车辆安全膨胀。 +- 安全余量只在碰撞检查时扩大车体矩形,防止地图膨胀与车体膨胀重复计算。 +- `ResolutionMm` 必须在 20~200 mm;地图最多 4,000,000 格,分配前使用 `checked` 检查。 +- 地图坐标采用 `[XMin, XMax) × [YMin, YMax)`;地图外始终按占据处理。 +- `PrimitiveLengthMeters=0.50` 表示原语最大长度;`IntegrationStepMeters=0.05` 表示积分最大步长。 +- 碰撞采样中心位移不得超过 `min(0.025 m, Map.ResolutionMeters / 2)`。 +- 默认终点容差为 0.15 m 和 5°。每个原语在内部积分点逐点检查目标,第一次满足条件即截断为终点候选。 +- 生成终点候选时不能立即成功;候选必须进入 Open List,作为当前最佳有效条目出队时才成功。 +- 第一版只支持恒曲率前进、倒车及原语边界换向;不支持蟹行、横移、原地旋转、Reeds-Shepp 精确连接、平滑、速度规划或底盘控制。 +- PowerShell 脚本首行设置 `$ErrorActionPreference = 'Stop'`,统一加载 `bin/Debug/netstandard2.0/ClumsyPilot.dll`。 +- 用户已明确不需要 Git 自检。本计划不包含 `git diff`、`git add`、`git commit` 等步骤;版本管理由用户另行处理。 ---- +## P0 与 P1 完成定义 -## 最终目录与职责 +| 阶段 | 必须完成的结果 | 阶段出口 | +| --- | --- | --- | +| P0-MAP:当前首要工作 | Map 文件结构、统一障碍来源、TwoLeg 投影、栅格化、距离场、快照缓存、PNG 迁移、自动测试和 `MovementTest.MapTest` 实际调用 | Map 独立构建成功;3 个 Map 脚本通过;Clumsy MapTest 只通过 `PlanningMapFactory.Create` 完成建图与显示 | +| P0-PLAN:Map Gate 之后 | CoarsePath 契约、保守碰撞、原语截断、Open List、Hybrid A*、输出校验和一次调用门面 | 6 个非 UI P0 脚本全部通过;固定场景可由 `CoarsePathPlanningService` 返回经最终复核的粗路径 | +| P1:集成与优化 | CoarsePath MovementTest、性能基准、旧地图退役和 README | 7 个功能脚本及 Release 基准通过;UI 入口只做调试展示;旧地图类型不再成为规划运行时入口 | -~~~text +执行顺序固定为“P0-MAP 独立闭环 → P0-PLAN → P1”。除了 Map 自己的 `MovementTest.MapTest` 和 PNG 调试辅助外,在 Map Gate 通过前不编写粗路径搜索或 CoarsePath UI,避免旧 `MovementTest.Trapmaptest.cs` 的传感器和渲染依赖进入核心模块。 + +## 当前首要里程碑:P0-MAP 独立闭环 + +当前阶段只处理 `Utils` 中被 Map 使用的无状态工具、完整 `Map` 目录、Map 自动化测试和 Map 实际调用。不得提前创建 `CoarsePath/Search`、`CoarsePath/Vehicle` 或 `CoarsePath/Output` 运行时代码。 + +### Map 实施顺序 + +1. 建立 `Map/Core`、`Map/Obstacles`、`Map/Sources`、`Map/Planning`、`Map/Test/Visualization` 目录和命名空间。 +2. 完成 `MapBoundsMm`、连续行优先 `EnvironmentGridMap`、圆/矩形 DTO 和唯一栅格化器。 +3. 完成统一 `IMapObstacleSource`、人工来源、TwoLeg 快照来源和事务式 `EnvironmentMapBuilder`。 +4. 完成 `PlanningGridMap`、精确 EDT、保守距离场和 mm→m 适配。 +5. 完成 `PlanningMapFactory`、容量 4 的两级快照缓存和地图变化判断。 +6. 将旧 `TrapMapImageExporter.cs` 能力拆到 `Map/Test/Visualization`,只消费只读快照。 +7. 完成三个 Map PowerShell 脚本和 `MovementTest.MapTest`,用真实入口验证建图、复用和显示。 + +对应详细任务的执行次序为:`Task 1 → Task 3 → Task 4 → Task 5 → Task 6 → Task 13 → Task 14 的 MapTest 部分 → Map Gate`。`Task 2` 和 `Task 7~12` 在 Map Gate 之后执行。 + +### Map 实际调用契约 + +`PlanningMapFactory` 应由长期存在的服务或测试对象持有,不能每次调用都重新 `new`,否则容量 4 的快照缓存无法跨调用复用: + +```csharp +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + +public PlanningMapBuildResult CreateCurrentMap( + MapBoundsMm bounds, + long manualVersion, + IReadOnlyList manualObstacles, + long twoLegVersion, + TwoLegProjectionInput twoLegSnapshot) +{ + return _mapFactory.Create(new PlanningMapRequest + { + Bounds = bounds, + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource( + "manual", manualVersion, true, manualObstacles), + new TwoLegObstacleSource( + "two-leg", twoLegVersion, false, twoLegSnapshot), + }, + AllowExplicitEmptyMap = false, + }); +} +``` + +调用方只判断统一结果,不接触 builder、rasterizer、投影器或 adapter: + +```csharp +PlanningMapBuildResult result = CreateCurrentMap( + bounds, manualVersion, manualObstacles, twoLegVersion, twoLegSnapshot); + +if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + return; + +PlanningGridMap map = result.Map; +bool worldOriginIsOccupied = map.IsOccupiedWorld(0d, 0d); +double worldOriginClearance = + map.GetConservativeObstacleDistanceMeters(0d, 0d); +``` + +### Map Gate + +以下条件必须全部满足,才开始 CoarsePath 契约和搜索: + +- [ ] 新 `Map` 运行时代码不读取 `TwoLegDetect`、定位、Painter、Toast、UI 或系统时间。 +- [ ] 人工、TwoLeg 以及未来来源全部经 `IMapObstacleSource.ProjectToWorld()` 输出世界 mm 几何,再由唯一 rasterizer 写图。 +- [ ] 地图不写 AMR 自身,不使用旧默认 300 mm 膨胀。 +- [ ] `PlanningMapFactory.Create` 对完整输入命中、占据命中和占据变化给出可区分结果。 +- [ ] `MovementTest.MapTest` 只调用 `PlanningMapFactory` 和可选 PNG exporter,不复制地图构造逻辑。 +- [ ] 下列命令全部通过: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +``` + +### P0-MAP 执行状态(2026-07-27) + +- [x] 已完成 Task 1、Task 3~6、Task 13,以及 Task 14 的 `MovementTest.MapTest` 部分。 +- [x] 已建立新的 `Map` 目录结构、统一障碍物来源与 TwoLeg 检测时位姿投影;Map 核心不读取传感器、UI 或系统时钟。 +- [x] 已完成只读规划快照、保守 EDT 距离场、容量 4 的 LRU 缓存、PNG 可视化拆分和实际 MapTest 调用。 +- [x] 已通过上述 Map Gate 命令;另外 `verify_planning_utils.ps1` 也已通过。 +- [x] 未执行 Git 自检、暂存或提交。 + +## 最终目录与文件职责 + +```text ClumsyPilot/ParkrobTrajplanner/ -├── Initial_plan/ -│ ├── AMR_轨迹规划技术路线_Agent版.md -│ └── AMR_HybridAStar_粗路径实施计划.md -├── Utils/ # MultiWheelC.TrajectoryPlanning.Utils -│ ├── AngleMath.cs # 角度归一化、角差、航向索引 -│ ├── UnitConverter.cs # mm/m、deg/rad、半径/曲率 -│ ├── CoordinateTransform.cs # 世界系与车体系二维变换 -│ ├── NumericGuard.cs # 数值合法性校验 -│ └── GridIndex.cs # 行列索引值对象 -├── Map/ # MultiWheelC.TrajectoryPlanning.Mapping +├── Initial_plan/ ------ 方案与实施文档,不放运行时代码 +├── Utils/ ------ 通用、无状态、确定性数值工具 +│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引 +│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率转换 +│ ├── CoordinateTransform.cs ------ 车体坐标与世界坐标二维刚体变换 +│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验 +│ └── GridIndex.cs ------ 不可变行列索引值对象 +├── Map/ ------ MultiWheelC.TrajectoryPlanning.Mapping │ ├── Core/ -│ │ ├── EnvironmentGridMap.cs # 仅外部障碍物的 mm 栅格 -│ │ ├── MapBuildRequest.cs # 环境图构造参数 -│ │ ├── EnvironmentMapBuildResult.cs # 环境图构造结果 -│ │ └── EnvironmentMapBuilder.cs # 内部构造器 +│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 占据栅格 +│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界 +│ │ ├── MapBuildRequest.cs ------ 内部环境图构建请求 +│ │ ├── EnvironmentMapBuildResult.cs ------ 环境图、来源摘要和失败原因 +│ │ └── EnvironmentMapBuilder.cs ------ 校验来源并事务式合并障碍图层 │ ├── Obstacles/ -│ │ ├── IMapObstacle.cs # 障碍物几何契约 -│ │ ├── AxisAlignedRectangleObstacle.cs # 中心、宽、高矩形 -│ │ ├── CircleObstacle.cs # 中心、半径圆形 -│ │ └── MapObstacleRasterizer.cs # 唯一占据写入器 +│ │ ├── IMapObstacle.cs ------ 世界坐标障碍物公共几何契约 +│ │ ├── AxisAlignedRectangleObstacle.cs ------ 轴对齐矩形障碍 DTO +│ │ ├── CircleObstacle.cs ------ 圆形障碍 DTO +│ │ └── MapObstacleRasterizer.cs ------ 唯一占据栅格写入器 │ ├── Sources/ -│ │ ├── ManualObstacleSource.cs # 人工障碍物集合 -│ │ └── TwoLegObstacleProjector.cs # TwoLegDetect 投影 +│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一接口 +│ │ ├── ObstacleSourceStatus.cs ------ Applied/Empty/Unavailable/Invalid +│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和障碍集合 +│ │ ├── ManualObstacleSource.cs ------ 输出人工圆和矩形 +│ │ ├── TwoLegProjectionInput.cs ------ 两腿端点、检测状态和检测位姿快照 +│ │ ├── TwoLegObstacleSource.cs ------ TwoLeg 统一来源适配器 +│ │ └── TwoLegObstacleProjector.cs ------ 车体系两腿端点投影到世界系 │ ├── Planning/ -│ │ ├── PlanningGridMap.cs # 只读 m 单位规划地图 -│ │ ├── ObstacleDistanceField.cs # 最近障碍距离 -│ │ └── PlanningMapAdapter.cs # mm 地图适配 -│ ├── PlanningMapRequest.cs # Map 统一输入 -│ ├── PlanningMapBuildResult.cs # Map 统一输出 -│ ├── PlanningMapFactory.cs # Map 唯一公共调用类 -│ └── Test/MovementTest.MapTest.cs # 最后阶段的地图测试入口 -├── CoarsePath/ # MultiWheelC.TrajectoryPlanning.CoarsePath -│ ├── Contracts/ -│ │ ├── Pose2D.cs # m/rad 位姿 -│ │ ├── VehicleParameters.cs # 尺寸、曲率、安全余量 -│ │ ├── PlanningRequest.cs # Planner 输入 -│ │ ├── HybridAStarConfiguration.cs # 可调参数 -│ │ ├── PlanningResult.cs # Planner 输出 -│ │ ├── PlanningStatus.cs # 成功/失败状态 -│ │ ├── PlanningDiagnostics.cs # 诊断统计 -│ │ ├── CoarsePathPoint.cs # 稠密空间路径点 -│ │ └── PathSegment.cs # 前进/倒车分段 -│ ├── Vehicle/ -│ │ ├── VehicleKinematics.cs # 最大曲率 -│ │ ├── HeadingFootprintTemplate.cs # 单航向车体模板 -│ │ ├── HeadingFootprintTemplateCache.cs # 模板预计算 -│ │ └── FootprintCollisionChecker.cs # 碰撞检查 -│ ├── Search/ -│ │ ├── MotionPrimitive.cs # 恒曲率原语 -│ │ ├── MotionPrimitiveGenerator.cs # 原语积分 -│ │ ├── HybridAStarNode.cs # 搜索节点 -│ │ ├── HybridAStarNodeKey.cs # Closed Set 键 -│ │ ├── GoalToleranceChecker.cs # 容差终点 -│ │ ├── GridDijkstraHeuristic.cs # 2D 启发 -│ │ └── HybridAStarSearch.cs # 搜索管理 -│ ├── Output/ -│ │ ├── PathBacktracker.cs # 恢复内部积分点 -│ │ ├── CoarsePathAssembler.cs # 弧长、换向、分段 -│ │ └── CoarsePathValidator.cs # 最终复核 -│ ├── HybridAStarPlanner.cs # CoarsePath 唯一公共调用类 +│ │ ├── PlanningGridMap.cs ------ 只读 m 占据图、距离场和快照元数据 +│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间精确二维欧氏距离变换 +│ │ ├── ObstacleDistanceField.cs ------ 不高估真实净空的距离下界 +│ │ ├── PlanningMapCache.cs ------ 容量 4 的线程安全两级快照缓存 +│ │ └── PlanningMapAdapter.cs ------ 占据深拷贝、mm→m 和距离场生成 +│ ├── PlanningMapRequest.cs ------ Map 模块统一输入 +│ ├── PlanningMapBuildResult.cs ------ Map 模块统一输出和缓存命中类型 +│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共创建入口 │ └── Test/ -│ ├── CoarsePathScenarioFactory.cs # 可复现场景 -│ └── MovementTest.CoarsePathTest.cs # 最后阶段的规划测试入口 -└── README.md # 两个门面类的调用说明 +│ ├── MovementTest.MapTest.cs ------ Clumsy UI 地图构建/显示入口 +│ └── Visualization/ +│ ├── PlanningMapImageExportRequest.cs ------ 快照、叠加层和输出选项 +│ ├── PlanningMapImageExportResult.cs ------ PNG 状态、路径、尺寸和诊断 +│ ├── PlanningMapImageExporter.cs ------ 校验、渲染编排和原子发布 +│ ├── PlanningMapImageRenderer.cs ------ 地图/车辆/路径绘制到 RGBA +│ └── ValidatedPngWriter.cs ------ Stb 编码、PNG 结构和 CRC 校验 +├── CoarsePath/ ------ MultiWheelC.TrajectoryPlanning.CoarsePath +│ ├── Contracts/ +│ │ ├── Pose2D.cs ------ m/rad 不可变二维位姿 +│ │ ├── TravelDirection.cs ------ Forward/Reverse +│ │ ├── GoalDirectionConstraint.cs ------ Any/Forward/Reverse +│ │ ├── VehicleParameters.cs ------ 尺寸、安全余量和最大曲率 +│ │ ├── PlanningRequest.cs ------ 纯规划器输入 +│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差 +│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和分段 +│ │ ├── PlanningStatus.cs ------ 输入、碰撞、搜索和验证状态 +│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计 +│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和净空 +│ │ ├── CoarsePathPointSource.cs ------ Start/MotionPrimitive/GoalTruncation +│ │ └── PathSegment.cs ------ 包含式方向分段索引 +│ ├── Vehicle/ +│ │ ├── VehicleKinematics.cs ------ 解析保守最大曲率 +│ │ ├── VehicleFootprint.cs ------ 扩大矩形、AABB 和外接圆 +│ │ ├── OrientedRectangleCellIntersection.cs ------ 旋转矩形与格矩形精确相交 +│ │ └── FootprintCollisionChecker.cs ------ 边界、快速放行、精确和扫掠检查 +│ ├── Search/ +│ │ ├── MotionPrimitive.cs ------ 恒曲率原语和实际截断长度 +│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分并保留内部采样点 +│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 确定性 Open List +│ │ ├── SearchCostCalculator.cs ------ 统一计算等效米搜索代价 +│ │ ├── HybridAStarNode.cs ------ 连续状态、代价、父索引和原语 +│ │ ├── HybridAStarNodeKey.cs ------ 位置/航向/方向/曲率离散键 +│ │ ├── GoalToleranceChecker.cs ------ 位置、航向和进入方向判断 +│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域、禁止切角的二维启发 +│ │ └── HybridAStarSearch.cs ------ 扩展、重开、限额和候选管理 +│ ├── Output/ +│ │ ├── PathBacktracker.cs ------ 根据父索引重建原语内部点 +│ │ ├── CoarsePathAssembler.cs ------ 弧长、换向点和方向分段 +│ │ └── CoarsePathValidator.cs ------ 数值、碰撞、曲率、终点和分段复核 +│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的下层门面 +│ ├── Facade/ +│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需全部输入 +│ │ ├── CoarsePathPlanningJobResult.cs ------ 地图结果与粗路径结果 +│ │ ├── PlanningDebugOptions.cs ------ 地图、路径、碰撞调试开关 +│ │ ├── IPlanningDebugSink.cs ------ 不改变规划状态的调试消费接口 +│ │ └── CoarsePathPlanningService.cs ------ 建图、搜索和调试的一次调用入口 +│ └── Test/ +│ ├── CoarsePathScenarioFactory.cs ------ 固定、可复现的地图与规划场景 +│ └── MovementTest.CoarsePathTest.cs ------ 后台规划并在 Clumsy UI 显示 +│ └── README.md ------ 粗规划模块边界、调用示例和文档链接 ClumsyPilot/tests/ -├── verify_planning_utils.ps1 -├── verify_planning_map_factory.ps1 -├── verify_planning_map_adapter.ps1 -├── verify_coarse_path_search.ps1 -└── verify_coarse_path_integration.ps1 -~~~ +├── verify_planning_utils.ps1 ------ 工具与公共契约 +├── verify_planning_map_factory.ps1 ------ 来源、事务、缓存与快照 +├── verify_planning_map_adapter.ps1 ------ 栅格、适配和距离场 +├── verify_planning_map_image.ps1 ------ PNG 渲染与原子发布 +├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞 +├── verify_coarse_path_search.ps1 ------ 原语截断、堆、代价和搜索 +├── verify_coarse_path_integration.ps1 ------ 一次调用端到端验证 +└── benchmark_coarse_path.ps1 ------ Release 性能与内存门槛 +``` -## 唯一调用方式 +## 固定公共调用方式 -~~~csharp -var mapResult = new PlanningMapFactory().Create(new PlanningMapRequest +常规业务代码只保留一个长期存在的服务实例: + +```csharp +var result = planningService.Plan(new CoarsePathPlanningJob { - ResolutionMm = 50f, - ManualObstacles = manualObstacles, - TwoLegInput = optionalTwoLegInput, - AllowExplicitEmptyMap = true, -}); - -if (!mapResult.Succeeded) - return PlanningResult.FromMapFailure(mapResult); - -return new HybridAStarPlanner().Plan(new PlanningRequest -{ - Map = mapResult.Map, + MapRequest = new PlanningMapRequest + { + Bounds = bounds, + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource( + "manual", manualVersion, true, manualObstacles), + new TwoLegObstacleSource("two-leg", twoLegVersion, false, twoLegSnapshot), + }, + AllowExplicitEmptyMap = true, + }, Start = startPose, Goal = goalPose, Vehicle = vehicle, Configuration = configuration, -}); -~~~ - -调用方不得直接创建 EnvironmentMapBuilder、MapObstacleRasterizer、PlanningMapAdapter、碰撞检查器、原语生成器或搜索节点。 - ---- - -### Task 1:建立 Utils 与工具测试 - -**文件:** -- Create:Utils 下的 AngleMath.cs、UnitConverter.cs、CoordinateTransform.cs、NumericGuard.cs、GridIndex.cs -- Test:ClumsyPilot/tests/verify_planning_utils.ps1 - -**输出:** NormalizeRadians、ShortestSignedAngleDifference、MillimetersToMeters、DegreesToRadians、TransformLocalToWorld 和 GridIndex。 - -- [ ] 写失败测试: - -~~~powershell -Assert-Equal 1.25 ([UnitConverter]::MillimetersToMeters([single]1250)) -Assert-Near 0.0 ([AngleMath]::NormalizeRadians(6.283185307179586)) 1e-12 -~~~ - -- [ ] 运行 verify_planning_utils.ps1;预期 FAIL,类型不存在。 -- [ ] 实现 UnitConverter: - -~~~csharp -public static double MillimetersToMeters(float millimeters) => millimeters / 1000d; -public static double DegreesToRadians(float degrees) => degrees * Math.PI / 180d; -~~~ - -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add Utils 与 verify_planning_utils.ps1;git commit -m "feat: add planning utilities"。 - -### Task 2:定义 CoarsePath 契约与可调容差 - -**文件:** -- Create:CoarsePath/Contracts 下的 Pose2D、VehicleParameters、PlanningRequest、HybridAStarConfiguration、PlanningResult、PlanningStatus、PlanningDiagnostics、CoarsePathPoint、PathSegment。 -- Test:verify_planning_utils.ps1 - -**输出:** 默认原语 0.50 m、积分 0.05 m、航向 5°、位置容差 0.15 m、航向容差 5°。 - -- [ ] 写失败测试,断言上述配置默认值和 Success、InvalidMap、StartInCollision、GoalInCollision、NoFeasiblePath 状态。 -- [ ] 运行脚本;预期 FAIL,配置类型不存在。 -- [ ] 实现不可变请求/结果契约;结果不得包含速度、加速度或时间。 -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add CoarsePath/Contracts 与 verify_planning_utils.ps1;git commit -m "feat: add coarse path contracts"。 - -### Task 3:实现外部障碍环境地图 - -**文件:** -- Create:Map/Core/EnvironmentGridMap.cs -- Create:Map/Obstacles 下的 IMapObstacle、AxisAlignedRectangleObstacle、CircleObstacle、MapObstacleRasterizer。 -- Create:Map/Sources/ManualObstacleSource.cs -- Test:verify_planning_map_factory.ps1 - -**输出:** 矩形按中心/宽/高,圆按中心/半径栅格化;环境地图没有车辆足迹接口。 - -- [ ] 写失败测试,分别验证矩形中心、圆心、边界外占据和不存在 MarkVehicleFootprint。 -- [ ] 运行脚本;预期 FAIL,地图和障碍物类型不存在。 -- [ ] 实现轴对齐矩形与圆-格矩形相交栅格化。 -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add Map/Core Map/Obstacles Map/Sources/ManualObstacleSource.cs 和测试;git commit -m "feat: add external obstacle map"。 - -### Task 4:实现 TwoLeg 图层和 PlanningMapFactory - -**文件:** -- Create:Map/Sources/TwoLegObstacleProjector.cs -- Create:Map/Core 下的 MapBuildRequest、EnvironmentMapBuildResult、EnvironmentMapBuilder。 -- Create:Map 下的 PlanningMapRequest、PlanningMapBuildResult、PlanningMapFactory。 -- Test:verify_planning_map_factory.ps1 - -**输出:** new PlanningMapFactory().Create(request) 是 Map 唯一公共入口;TwoLeg 失败不能删除人工地图。 - -- [ ] 写失败测试,验证人工地图成功时 TwoLegApplied 为 false 仍然成功。 -- [ ] 运行脚本;预期 FAIL,PlanningMapFactory 不存在。 -- [ ] 实现工厂:合并人工障碍与可选投影;TwoLegObstacleProjector 只产出 CircleObstacle。 -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add Map 与 verify_planning_map_factory.ps1;git commit -m "feat: add planning map factory"。 - -### Task 5:适配 PlanningGridMap 并生成距离场 - -**文件:** -- Create:Map/Planning 下的 PlanningGridMap、ObstacleDistanceField、PlanningMapAdapter。 -- Modify:Map/PlanningMapFactory.cs -- Test:verify_planning_map_adapter.ps1 - -**输出:** 深拷贝占据格,mm 转 m,越界占据,生成最近障碍距离场。 - -- [ ] 写失败测试,断言 ResolutionMeters=0.05、障碍坐标转换、越界和障碍格距离为 0。 -- [ ] 运行脚本;预期 FAIL,适配类型不存在。 -- [ ] 实现 PlanningMapAdapter.Adapt(EnvironmentGridMap) 和多源距离场;显式空旷图使用正无穷距离。 -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add Map/Planning Map/PlanningMapFactory.cs 和测试;git commit -m "feat: adapt planning map"。 - -### Task 6:实现车辆足迹与碰撞检查 - -**文件:** -- Create:CoarsePath/Vehicle 下的 VehicleKinematics、HeadingFootprintTemplate、HeadingFootprintTemplateCache、FootprintCollisionChecker。 -- Test:verify_coarse_path_search.ps1 - -**输出:** IsCollisionFree(Pose2D, PlanningGridMap, VehicleParameters, out clearanceMeters)。 - -- [ ] 写失败测试,覆盖占据碰撞、45° 安全、越界碰撞和不同航向。 -- [ ] 运行脚本;预期 FAIL,碰撞类型不存在。 -- [ ] 实现扩大车体模板;距离场仅在大于外接圆时放行,否则查询航向模板。 -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add CoarsePath/Vehicle 和测试;git commit -m "feat: add footprint collision checking"。 - -### Task 7:生成恒曲率运动原语 - -**文件:** -- Create:CoarsePath/Search/MotionPrimitive.cs、MotionPrimitiveGenerator.cs -- Test:verify_coarse_path_search.ps1 - -**输出:** 五级曲率、前进/倒车、最多 0.50 m、每步不超过 0.05 m、保留内部点。 - -- [ ] 写失败测试,覆盖直行 10 点、曲线航向变化、倒车和曲率跳变拒绝。 -- [ ] 运行脚本;预期 FAIL,原语生成器不存在。 -- [ ] 实现积分: - -~~~csharp -x += directionSign * Math.Cos(theta) * stepMeters; -y += directionSign * Math.Sin(theta) * stepMeters; -theta = AngleMath.NormalizeRadians(theta + directionSign * curvature * stepMeters); -~~~ - -- [ ] 构建并重跑脚本;预期 PASS,所有内部点经过碰撞检查。 -- [ ] 提交:git add 两个原语文件和测试;git commit -m "feat: add motion primitives"。 - -### Task 8:实现目标容差、启发和 Hybrid A* 搜索 - -**文件:** -- Create:CoarsePath/Search 下的 HybridAStarNode、HybridAStarNodeKey、GoalToleranceChecker、GridDijkstraHeuristic、HybridAStarSearch。 -- Test:verify_coarse_path_search.ps1 - -**输出:** Closed Set 键为位置格、航向格、方向、曲率索引;终点读取可调容差。 - -- [ ] 写失败测试,无障碍前进、单障碍绕行、一次倒车、容差成功、无解和节点上限。 -- [ ] 运行脚本;预期 FAIL,搜索类型不存在。 -- [ ] 实现搜索循环: - -~~~csharp -while (openList.Count > 0 && diagnostics.ExpandedNodeCount < configuration.MaxExpandedNodes) -{ - var current = PopBestValidNode(); - if (_goalChecker.IsReached(current.Pose, request.Goal, configuration, current.Direction)) - return SearchResult.Succeeded(current.NodeIndex); - ExpandValidPrimitives(current); -} -~~~ - -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add CoarsePath/Search 和测试;git commit -m "feat: add hybrid astar search"。 - -### Task 9:回溯、输出和 HybridAStarPlanner 门面 - -**文件:** -- Create:CoarsePath/Output 下的 PathBacktracker、CoarsePathAssembler、CoarsePathValidator。 -- Create:CoarsePath/HybridAStarPlanner.cs -- Test:verify_coarse_path_integration.ps1 - -**输出:** new HybridAStarPlanner().Plan(request) 返回稠密路径、换向标记、分段、诊断和状态。 - -- [ ] 写失败测试,验证首点弧长为 0、内部点连续、换向标记、终点容差和最终复核。 -- [ ] 运行脚本;预期 FAIL,门面不存在。 -- [ ] 实现编排: - -~~~csharp -var search = _search.Run(request); -if (!search.Succeeded) return PlanningResult.Failed(search.Status, search.Diagnostics); -var points = _assembler.Assemble(_backtracker.Backtrack(search)); -return _validator.Validate(points, request) - ? PlanningResult.Succeeded(points, search.Diagnostics) - : PlanningResult.Failed(PlanningStatus.FinalValidationFailed, search.Diagnostics); -~~~ - -- [ ] 构建并重跑脚本;预期 PASS。 -- [ ] 提交:git add CoarsePath/Output CoarsePath/HybridAStarPlanner.cs 和测试;git commit -m "feat: expose coarse path planner"。 - -### Task 10:完成非 UI 回归与调用说明 - -**文件:** -- Create:ParkrobTrajplanner/README.md -- Modify:全部 verify_planning 脚本。 - -- [ ] 写失败测试,断言 PlanningMapFactory 和 HybridAStarPlanner 是唯一由外部测试实例化的模块门面。 -- [ ] 运行全部脚本;预期在 README、状态或门面断言未齐全时 FAIL。 -- [ ] 在 README 写入 Map 到 Planner 的调用代码、mm/m-rad 边界和非目标。 -- [ ] 构建、运行全部规划脚本并运行 git diff --check;预期全部 PASS。 -- [ ] 提交:git add README 和 tests;git commit -m "test: complete coarse path regression coverage"。 - -### Task 11:最后编写 MovementTest 集成模块 - -**文件:** -- Create:Map/Test/MovementTest.MapTest.cs -- Create:CoarsePath/Test/CoarsePathScenarioFactory.cs -- Create:CoarsePath/Test/MovementTest.CoarsePathTest.cs -- Test:Clumsy UI 手动运行两个 MovementTest,然后重跑 Task 10 自动化回归。 - -**输出:** MapTest 只调用 PlanningMapFactory;CoarsePathTest 只调用两个门面;两者绝不发送底盘命令。 - -- [ ] 写失败测试,场景工厂生成的地图请求可由 PlanningMapFactory 构建,规划请求可由 HybridAStarPlanner 成功规划。 -- [ ] 运行 verify_coarse_path_integration.ps1;预期 FAIL,场景工厂不存在。 -- [ ] 实现入口: - -~~~csharp -[MovementTest(name = "Hybrid A* 粗路径测试")] -public sealed class CoarsePathTest : MovementTest -{ - public override void Test() + StartVehicleCurvature = 0d, + StartDirection = null, + GoalDirection = GoalDirectionConstraint.Any, + Debug = new PlanningDebugOptions { - var map = new PlanningMapFactory().Create(CoarsePathScenarioFactory.CreateSingleObstacleMapRequest()); - var result = map.Succeeded - ? new HybridAStarPlanner().Plan(CoarsePathScenarioFactory.CreatePlanningRequest(map.Map)) - : PlanningResult.FromMapFailure(map); - DrawMapAndPath(map, result); - } + VisualizeMap = true, + VisualizePath = true, + VisualizeCollisionChecks = false, + }, +}, cancellationToken); +``` + +已有地图快照复用或纯搜索测试才直接调用 `HybridAStarPlanner.Plan(PlanningRequest, CancellationToken)`。业务调用方不得直接拼装 builder、rasterizer、碰撞器、运动原语或搜索节点。 + +## P0:正确可用 + +### Task 1:建立 Utils 与测试基座 + +**文件:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs` +- Create: `ClumsyPilot/tests/verify_planning_utils.ps1` + +**产出接口:** + +```csharp +double AngleMath.NormalizeRadians(double radians); +double AngleMath.ShortestSignedDifference(double from, double to); +int AngleMath.ToHeadingIndex(double heading, double resolution, int binCount); +double UnitConverter.MillimetersToMeters(double value); +double UnitConverter.DegreesToRadians(double value); +bool NumericGuard.IsFinite(double value); +readonly struct GridIndex { int Row; int Col; } +``` + +- [ ] 写失败测试:断言 `2π→0`、`179°→-179°` 最短角差为 `2°`、`1250 mm→1.25 m`、车体系 `(1000,0)` 在世界位姿 `(2000,3000,90°)` 后得到 `(2,4) m`,并拒绝 NaN/Infinity。 +- [ ] 执行以下命令,预期构建成功、脚本因新类型不存在返回非零。 + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +``` + +- [ ] 实现:角度统一到 `[-π,π)`;航向索引先归一化再 `floor`;刚体变换使用 `world=origin+R(heading)×local`;`GridIndex` 实现值相等和稳定哈希。 +- [ ] 重跑脚本,预期输出 `Planning utility checks passed.`。 + +### Task 2:锁定 CoarsePath 公共契约与默认参数 + +**文件:** + +- Create: `CoarsePath/Contracts/` 下目录树列出的 12 个契约文件 +- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` + +**固定默认值:** + +```csharp +PrimitiveLengthMeters = 0.50; +IntegrationStepMeters = 0.05; +MaximumCollisionCheckStepMeters = 0.025; +HeadingResolutionRadians = Math.PI / 36d; +CurvatureLevelCount = 5; +GoalPositionToleranceMeters = 0.15; +GoalHeadingToleranceRadians = Math.PI / 36d; +MaximumExpandedNodes = 200000; +SearchTimeout = TimeSpan.FromSeconds(5); +HeuristicWeight = 1.0; +ReverseCostMultiplier = 1.5; +GearSwitchPenaltyMeters = 1.0; +CurvatureMagnitudeWeight = 0.10; +CurvatureChangePenaltyMetersPerLevel = 0.05; +ClearanceCostWeight = 0.20; +ClearanceCostDistanceMeters = 0.50; +``` + +- [ ] 扩展失败测试:断言上述默认值,三个方向/来源枚举,以及 `PlanningRequest` 的起始曲率、起始方向、目标进入方向。 +- [ ] 断言 `PlanningStatus` 包含:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 +- [ ] 实现不可变 `Pose2D`;结果工厂只允许成功结果携带路径,失败结果路径为空。 +- [ ] `PlanningDiagnostics` 固定记录扩展节点数、生成节点数、重新打开节点数、陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因。 +- [ ] `CoarsePathPoint` 固定包含 `X/Y`、`Heading/UnwrappedHeading`、`ArcLength`、`Direction`、`VehicleCurvature`、`BodyClearance`、`IsGearSwitchPoint` 和 `Source`;`PathSegment` 固定包含方向和包含式 `StartIndex/EndIndex`。 +- [ ] 重跑工具脚本,预期契约和默认值全部通过。 + +### Task 3:实现环境栅格、边界与统一栅格化 + +**文件:** + +- Create: `Map/Core/MapBoundsMm.cs` +- Create: `Map/Core/EnvironmentGridMap.cs` +- Create: `Map/Obstacles/IMapObstacle.cs` +- Create: `Map/Obstacles/AxisAlignedRectangleObstacle.cs` +- Create: `Map/Obstacles/CircleObstacle.cs` +- Create: `Map/Obstacles/MapObstacleRasterizer.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` + +上述 Map/CoarsePath 相对路径均位于 `ClumsyPilot/ParkrobTrajplanner/`。 + +- [ ] 写边界和栅格失败测试:20/200 mm 合法,范围外失败;4,000,001 格分配前失败;`XMax/YMax` 排他;非完整末格裁剪;地图外占据;圆/矩形与格边或格角接触时保守占据;完全在地图外的合法障碍不写格。 +- [ ] 运行脚本,预期新 Map 类型不存在。 +- [ ] 实现私有行优先 `byte[]`,索引固定为 `row*Cols+col`;世界转格使用 `floor((value-min)/resolution)`;不得返回内部缓冲区。 +- [ ] 实现唯一栅格化器:圆使用“圆心到格矩形最近点距离”,轴对齐矩形使用闭区间相交,只遍历裁剪后的候选包围盒。 +- [ ] 确认不存在 `MarkVehicleFootprint` 或安全距离参数,重跑脚本通过。 + +### Task 4:统一人工与 TwoLeg 障碍来源 + +**文件:** + +- Create: `Map/Sources/` 下目录树列出的 7 个文件 +- Create: `Map/Core/MapBuildRequest.cs` +- Create: `Map/Core/EnvironmentMapBuildResult.cs` +- Create: `Map/Core/EnvironmentMapBuilder.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_factory.ps1` + +**固定接口:** + +```csharp +public interface IMapObstacleSource +{ + string SourceId { get; } + long SourceVersion { get; } + bool IsRequired { get; } + ObstacleProjectionResult ProjectToWorld(); } -~~~ +``` -- [ ] MapTest 绘制人工矩形、人工圆、TwoLeg 投影和边界;CoarsePathTest 绘制起点、目标、路径、换向点和失败状态。TestStop 只清理 Painter/任务。 -- [ ] 构建、运行全部 PowerShell 脚本,并在 Clumsy UI 手动运行“规划地图测试”和“Hybrid A* 粗路径测试”;预期只显示调试结果,不发送运动命令。 -- [ ] 提交:git add Map/Test CoarsePath/Test 和 tests;git commit -m "feat: add coarse path movement tests"。 +- [ ] 构造函数统一为 `ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles)` 和 `TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input)`;计划内所有实际调用均使用这两个签名。 +- [ ] 写来源事务失败测试:人工 `Applied/Empty`、TwoLeg 两个圆、可选来源 `Unavailable/Invalid` 保留人工图层、必需来源失败导致整图失败、重复 `SourceId` 失败、输入顺序不同但结果相同。 +- [ ] 校验 `SourceId` 非空且一次请求内唯一,`SourceVersion` 非负;来源快照内容发生变化时,上层必须递增版本。 +- [ ] 写 TwoLeg 坐标测试:使用检测时车辆位姿完成车体系 mm 到世界系 mm 变换,不得使用规划开始时位姿。 +- [ ] 实现纯快照投影:`ProjectToWorld()` 不能调用 `TwoLegDetect`、定位、UI 或系统时间;过期判断由上层采集适配器在构造 DTO 前完成。 +- [ ] builder 按 `SourceId` 排序;可选失败只记诊断;必需失败不发布地图;全部成功几何交给唯一 rasterizer。 +- [ ] 重跑工厂脚本,预期来源状态、投影和事务断言通过。 -## 实施前检查 +### Task 5:生成不可变 PlanningGridMap 和保守距离场 -- PlanningMapFactory 与 HybridAStarPlanner 是未来调用方和测试入口唯一允许直接实例化的模块类。 -- MovementTest 位于最后一个任务;它之前所有 Map 与搜索逻辑必须已完成非 UI 自动化验证。 +**文件:** +- Create: `Map/Planning/EuclideanDistanceTransform.cs` +- Create: `Map/Planning/ObstacleDistanceField.cs` +- Create: `Map/Planning/PlanningGridMap.cs` +- Create: `Map/Planning/PlanningMapAdapter.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` + +- [ ] 扩展失败测试:mm→m、占据深拷贝、地图外距离为零、障碍格距离为零、空图距离正无穷、末格裁剪,以及所有样本距离不高于暴力几何距离。 +- [ ] 实现两次一维平方距离变换,复杂度 `O(Rows×Cols)`;不得逐自由格遍历全部障碍格。 +- [ ] 对精确栅格中心距离应用: + +```csharp +Math.Max(0d, centerDistanceMeters - Math.Sqrt(2d) * resolutionMeters) +``` + +- [ ] 查询使用点所在格的保守值,不做可能抬高结果的插值;空图跳过 EDT;边界检查独立执行。 +- [ ] `PlanningGridMap` 私有保存连续占据/距离数组,不提供写入口和缓冲区引用。 +- [ ] 重跑地图适配脚本,预期栅格、深拷贝、空图和保守距离全部通过。 + +### Task 6:实现 PlanningMapFactory 与两级快照缓存 + +**文件:** + +- Create: `Map/Planning/PlanningMapCache.cs` +- Create: `Map/PlanningMapRequest.cs` +- Create: `Map/PlanningMapBuildResult.cs` +- Create: `Map/PlanningMapFactory.cs` +- Modify: `Map/Planning/PlanningGridMap.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` + +```csharp +public sealed class PlanningMapFactory +{ + public PlanningMapBuildResult Create(PlanningMapRequest request); +} +``` + +`PlanningMapRequest` 固定包含 `MapBoundsMm Bounds`、`float ResolutionMm`、`IReadOnlyList ObstacleSources`、`bool AllowExplicitEmptyMap`。 + +- [ ] 写缓存测试:完全相同请求返回同一 `PlanningGridMap`;来源版本变化但最终占据未变化时产生新 `SnapshotId` 并复用占据/距离缓冲;占据变化时重建距离场。 +- [ ] 写规划可用性测试:至少一个成功来源提供有效障碍语义时 `PlanningReady=true`;所有来源均为空时仅 `AllowExplicitEmptyMap=true` 可用;必需来源失败或未明确空图语义时 `PlanningReady=false` 并填写 `PlanningBlockReason`,规划器随后返回 `MapNotReady`。 +- [ ] 验证失效边界:起终点、车辆、规划参数和可视化开关不进入地图指纹;边界、分辨率、空图策略、来源状态/版本或规范化几何变化必须进入。 +- [ ] 实现确定性 `InputFingerprint`:来源先按 ID 排序,字段固定顺序,浮点按 IEEE 位模式;命中后仍比较规范化结构。 +- [ ] 栅格化后计算 `OccupancyHash`;命中后仍比较地图几何、数组长度和逐字节内容,不能只信任哈希。 +- [ ] 实现容量 4 的线程安全 LRU;完整命中返回同一快照,占据命中共享不可变缓冲但生成新元数据,只有占据变化才运行 EDT。 +- [ ] `PlanningMapBuildResult` 固定返回 `Succeeded`、失败原因、来源摘要、缓存命中类型和成功时的 `PlanningGridMap`;快照固定保存 `PlanningReady`、`PlanningBlockReason`、来源版本摘要、`InputFingerprint`、`OccupancyHash`、单调 `SnapshotId`。 +- [ ] 增加 16 个并发相同请求测试和 LRU 淘汰测试,重跑脚本通过。 + +### Task 7:实现连续车体足迹和保守碰撞检查 + +**文件:** + +- Create: `CoarsePath/Vehicle/` 下目录树列出的 4 个文件 +- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` + +**固定接口:** + +```csharp +bool IsPoseCollisionFree( + Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, + double additionalMarginMeters, out double bodyClearanceMeters); + +bool IsSweptMotionCollisionFree( + Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, + double maximumCenterStepMeters, out double minimumBodyClearanceMeters); +``` + +- [ ] 写失败测试:正交、45°、任意航向、栅格中心/亚栅格中心、边角接触、薄障碍、地图边界、距离场快速放行,以及两个无碰撞端点之间有障碍的扫掠场景。 +- [ ] 实现以几何中心为参考的扩大车体;安全余量加到长度和宽度两侧;最大曲率与最小转弯半径并存时取更保守限制。 +- [ ] 使用分离轴定理精确判断连续旋转矩形与占据格矩形相交,接触视为碰撞;不得使用离散航向模板。 +- [ ] 检查顺序:扩大车体边界 → 保守距离严格大于外接圆时快速放行 → AABB 内占据格 SAT。 +- [ ] 扫掠采样中心步长不超过 `min(configuredStep,map.Resolution/2)`;每段临时附加余量为: + +```text +0.5 × (centerDisplacement + + circumscribedRadius × abs(headingDelta)) +``` + +- [ ] 重跑碰撞脚本,预期所有亚栅格、擦边和扫掠案例通过。 + +### Task 8:实现解析恒曲率原语和内部终点截断 + +**文件:** + +- Create: `CoarsePath/Search/MotionPrimitive.cs` +- Create: `CoarsePath/Search/MotionPrimitiveGenerator.cs` +- Create: `CoarsePath/Search/GoalToleranceChecker.cs` +- Create/Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**解析积分:** + +```csharp +double signedDistance = direction == TravelDirection.Forward ? step : -step; +double nextHeading = AngleMath.NormalizeRadians( + heading + curvature * signedDistance); + +if (Math.Abs(curvature) < 1e-12) +{ + nextX = x + signedDistance * Math.Cos(heading); + nextY = y + signedDistance * Math.Sin(heading); +} +else +{ + nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; + nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; +} +``` + +- [ ] 写原语测试:直行、圆弧、倒车、五级曲率、相邻曲率最多变化一级、最大长度 0.50 m、实际采样步长不超过 `min(IntegrationStep,CollisionStep,MapResolution/2)`。 +- [ ] 写用户提出的案例:目标距起点 0.30 m、原语最大 0.50 m、收紧容差;断言第一个满足目标的内部点截断,后续点不生成,来源为 `GoalTruncation`。 +- [ ] 每个内部点严格按“有限值 → 扫掠碰撞 → 目标条件”检查;碰撞必须先于目标。 +- [ ] 起点已满足目标时创建零长度候选,不生成原语。 +- [ ] 重跑搜索脚本,预期几何、碰撞采样和 0.30 m 截断案例通过。 + +### Task 9:实现 Open List、统一代价和二维启发 + +**文件:** + +- Create: `CoarsePath/Search/BinaryMinHeap.cs` +- Create: `CoarsePath/Search/SearchCostCalculator.cs` +- Create: `CoarsePath/Search/GridDijkstraHeuristic.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**固定代价:** + +```text +primitiveCost = + lengthMeters + × directionMultiplier + × (1 + + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature) + + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters)) + + gearSwitchPenalty + + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta) +``` + +- [ ] 写堆顺序测试:较小 `F`、较小 `H`、较大 `G`、较小插入序号;相同输入重复运行顺序一致。 +- [ ] 写代价测试:前进、倒车、换向、曲率幅值、曲率变化和净空项;拒绝负数及非有限权重。 +- [ ] 写 Dijkstra 测试:八邻域直/斜代价;两个正交邻格任一占据时禁止对角切角;二维不可达返回明确状态。 +- [ ] 实现专用二叉最小堆,不依赖 `PriorityQueue`;允许旧条目由搜索层惰性丢弃。 +- [ ] `HeuristicWeight=1` 使用 `F=G+H`;大于 1 时只承诺可行性。 +- [ ] 重跑搜索脚本,预期顺序、公式和切角限制通过。 + +### Task 10:实现 Hybrid A* 节点、重开和终点候选管理 + +**文件:** + +- Create: `CoarsePath/Search/HybridAStarNode.cs` +- Create: `CoarsePath/Search/HybridAStarNodeKey.cs` +- Create: `CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +- [ ] 写搜索测试:空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、`±π` 容差、无解、取消、超时、节点上限、重开和确定性。 +- [ ] 写候选排序测试:先生成较大 `F` 的终点候选时不得结束;较小 `F` 普通节点先出队;候选成为最佳有效条目后才成功。 +- [ ] 离散键固定为位置格、航向格、方向和曲率等级;连续位姿保留在节点中。 +- [ ] `Dictionary` 保存普通状态最佳 `G`;更小 `G` 允许重开;旧普通堆条目惰性丢弃。 +- [ ] 终点候选放入同一堆,但不能仅因另一个连续位姿落入相同离散键且 `G` 更低而被删除;候选出队时重新验证目标和末段碰撞。 +- [ ] 搜索循环在每次扩展前按顺序检查取消、5 s 超时、200,000 节点上限,再弹出有效条目;Open List 为空返回 `NoFeasiblePath`。 +- [ ] 重跑搜索脚本,预期候选顺序、重开、限额和所有场景通过。 + +### Task 11:回溯、装配、最终复核和 HybridAStarPlanner + +**文件:** + +- Create: `CoarsePath/Output/PathBacktracker.cs` +- Create: `CoarsePath/Output/CoarsePathAssembler.cs` +- Create: `CoarsePath/Output/CoarsePathValidator.cs` +- Create: `CoarsePath/HybridAStarPlanner.cs` +- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +```csharp +public sealed class HybridAStarPlanner +{ + public PlanningResult Plan( + PlanningRequest request, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] 写失败状态测试:请求、地图就绪、车辆参数、曲率配置、起终点越界/碰撞,以及搜索失败到结果的无异常映射。 +- [ ] 写输出测试:首点弧长零;弧长不递减;`UnwrappedHeading` 连续;终点截断来源正确;除换向对外无相邻重复点。 +- [ ] 写换向分段测试:换向处保留两个坐标/航向/弧长相同而方向不同的点;新方向点标记 `IsGearSwitchPoint`;包含式分段完整覆盖路径。 +- [ ] 搜索节点只存父索引、方向、曲率和实际原语长度;成功后使用相同解析积分和有效步长重建内部点。 +- [ ] 最终复核有限数值、曲率、扫掠碰撞、目标容差/方向、弧长、换向对和分段;失败返回 `FinalValidationFailed` 且不发布部分路径。 +- [ ] 重跑碰撞、搜索和集成脚本,预期全部通过。 + +### Task 12:实现一次调用 CoarsePathPlanningService + +**文件:** + +- Create: `CoarsePath/Facade/` 下目录树列出的 5 个文件 +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +```csharp +public sealed class CoarsePathPlanningService +{ + public CoarsePathPlanningJobResult Plan( + CoarsePathPlanningJob job, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] 写一次调用测试:固定执行 `PlanningMapFactory.Create → HybridAStarPlanner.Plan → debug sink`;地图失败不启动搜索;结果同时保留地图和规划结果。 +- [ ] 写旁路隔离测试:可视化开关不改变地图指纹、占据哈希、规划状态或路径;debug sink 异常只写调试诊断。 +- [ ] 服务持有同一 `PlanningMapFactory`,多次调用共享容量 4 缓存;默认 debug sink 为空行为。 +- [ ] 重跑以下 P0 验收,预期构建成功且 6 个脚本退出码为 0。 + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +## P0-MAP 收尾与 P1:集成、迁移和性能 + +### Task 13(P0-MAP):拆分迁移 TrapMapImageExporter + +**现有来源:** + +- Read/Migrate: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` + +**新文件:** + +- Create: `Map/Test/Visualization/PlanningMapImageExportRequest.cs` +- Create: `Map/Test/Visualization/PlanningMapImageExportResult.cs` +- Create: `Map/Test/Visualization/PlanningMapImageExporter.cs` +- Create: `Map/Test/Visualization/PlanningMapImageRenderer.cs` +- Create: `Map/Test/Visualization/ValidatedPngWriter.cs` +- Create: `ClumsyPilot/tests/verify_planning_map_image.ps1` + +- [ ] 写新图片测试:输入只允许 `PlanningGridMap`;覆盖关闭导出、非法尺寸、唯一命名、临时文件清理、PNG 签名/IHDR/IEND 和每个 chunk CRC。 +- [ ] 保留现有有效限制:`PixelsPerCell=4`、`MaximumImageEdgePixels=4000`、`MaximumFileSizeBytes=50 MiB`、`OutputDpi=300`、最多 1024 次重名重试、StbImageWriteSharp 1.16.7。 +- [ ] 按职责迁移:Request/Result 只放 DTO;Renderer 生成 RGBA;Writer 负责编码/CRC;Exporter 校验、独占临时文件和原子发布。 +- [ ] 新导出器不得依赖 `GridMapData`、`TrapMapVehiclePose`、TwoLeg 状态、传感器或地图构造器;车辆、起终点和路径只作可选叠加层。 +- [ ] 运行图片脚本通过后先保留旧文件,Task 16 确认等价覆盖后再退役。 + +### Task 14:增加地图与粗路径 MovementTest + +**文件:** + +- Create: `Map/Test/MovementTest.MapTest.cs` +- Create: `CoarsePath/Test/CoarsePathScenarioFactory.cs` +- Create: `CoarsePath/Test/MovementTest.CoarsePathTest.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +- [ ] **P0-MAP 部分:** 先创建 `MovementTest.MapTest.cs`。它只调用长期持有的 `PlanningMapFactory`,显示来源状态、栅格、快照 ID 和缓存命中,并可选调用新 PNG 导出器;通过 Map Gate 后即可结束当前首要里程碑。 +- [ ] **P1 部分:** Map Gate 和 P0-PLAN 均通过后,再创建 `CoarsePathScenarioFactory.cs` 与 `MovementTest.CoarsePathTest.cs`。 +- [ ] 场景工厂提供显式空图、单矩形绕行、人工圆+矩形+TwoLeg、相同地图缓存命中、倒车换向和无解案例。 +- [ ] CoarsePath 案例使用同一 `CoarsePathPlanningService`;测试类不得直接实例化 rasterizer、碰撞器、原语生成器或搜索节点。 +- [ ] `MovementTest.CoarsePathTest` 在后台任务调用同步 `Plan`,绘制起点、目标、路径、换向点和扩大车体检查点。 +- [ ] `TestStop` 先取消专用 `CancellationTokenSource`,再清理任务和 Painter;两个入口不得发送底盘运动命令。 +- [ ] Clumsy UI 手动运行时不阻塞界面,停止后无后台规划残留,调试开关不改变结果。 + +### Task 15(P1):性能、资源和确定性验收 + +**文件:** + +- Create: `ClumsyPilot/tests/benchmark_coarse_path.ps1` +- Modify: `Map/Planning/PlanningMapCache.cs` +- Modify: `Map/Planning/EuclideanDistanceTransform.cs` +- Modify: `CoarsePath/Search/BinaryMinHeap.cs` +- Modify: `CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `CoarsePath/Contracts/PlanningDiagnostics.cs` + +- [ ] 基准脚本输出地图规模、缓存命中、状态、耗时、扩展/生成/重开节点、陈旧堆条目、Open List 峰值和托管内存增量,超限返回非零。 +- [ ] 地图参考:20 m×20 m、0.05 m、160,000 格、100 障碍;20 次后完整构建 P95≤200 ms,完整缓存命中 P95≤5 ms。 +- [ ] 地图极限:4,000,000 格、100 障碍;3 s 内成功或明确失败;成功时内存增量≤160 MB,无溢出和部分快照。 +- [ ] 规划参考:12 m×8 m、0.05 m、矩形阻断直线、距离≥8 m;20 次 P95≤2 s,内存增量≤256 MB。 +- [ ] 规划压力:20 m×20 m、0.05 m;成功或无解均在 5 s、200,000 节点和 512 MB 增量内返回。 +- [ ] 优化只针对查询分配、重复 EDT、堆扩容和稠密点保存;不得降低碰撞保守性、跳过最终复核或放宽失败状态。 +- [ ] 运行: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj -c Release --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\benchmark_coarse_path.ps1 -Configuration Release +``` + +预期:全部指标达标,脚本退出码为 0。 + +### Task 16(P1):退役旧地图入口并完成文档 + +**文件:** + +- Modify/Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/MovementTest.Trapmaptest.cs` +- Delete after equivalent coverage: `ClumsyPilot/ParkrobTrajplanner/Occupancygird_Map/Map_test/TrapMapImageExporter.cs` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_grid.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_lifecycle.ps1` +- Update/Delete after equivalent coverage: `ClumsyPilot/tests/verify_trapmap_image.ps1` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` + +- [ ] 建立覆盖表:`TrapMapBounds→MapBoundsMm`、`GridMapData→EnvironmentGridMap/PlanningGridMap`、`TrapMapLayerComposer→EnvironmentMapBuilder`、`TrapMapBuilder.Get→上层采集+CoarsePathPlanningService`、旧 exporter→五个 Visualization 文件。 +- [ ] 等价脚本全部通过后再删除旧实现;不保留车辆写图、默认 300 mm 膨胀或地图构建器直接调用 `TwoLegDetect` 的兼容开关。 +- [ ] README 写明一次调用、下层门面、mm/m-rad 边界、快照变化判断、调试开关不参与指纹、P0/P1 命令和非目标。 +- [ ] 执行旧引用搜索: + +```powershell +rg -n "GridMapData|TrapMapBuilder|TrapMapLayerComposer|TrapMapImageExporter|MarkVehicleFootprint" .\ClumsyPilot +``` + +预期:仅迁移说明或历史文档可命中;运行时代码和新测试不得命中旧类型。 + +- [ ] 运行最终回归: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:Debug 构建成功,7 个功能脚本退出码均为 0;随后重跑 Task 15 Release 基准并通过。 + +## 最终验收清单 + +- [ ] `CoarsePathPlanningService.Plan(job)` 是推荐的一次调用入口。 +- [ ] `PlanningMapFactory` 和 `HybridAStarPlanner` 仅作为可独立测试的下层门面。 +- [ ] 人工、TwoLeg 和未来障碍通过同一个 `IMapObstacleSource` 进入唯一栅格化器。 +- [ ] 地图未变化时复用完整快照或占据/距离缓冲;可视化、起终点和车辆参数不参与地图变化判断。 +- [ ] 地图不包含车辆自身和安全膨胀,碰撞检查使用扩大车辆矩形。 +- [ ] 距离场是净空下界,不能因高估而跳过精确碰撞。 +- [ ] 0.50 m 原语可在任意内部采样点截断;终点候选按 Open List 顺序出队后才终止。 +- [ ] best-G、重开、陈旧条目、候选保护和堆排序均有自动化测试。 +- [ ] 输出包含稠密点、保守净空、实际曲率、换向点和完整方向分段。 +- [ ] PNG 和 MovementTest 只消费只读快照,不成为核心规划依赖。 +- [ ] 失败、取消、超时和限额均返回空路径及明确状态,不发布部分结果。 +- [ ] Debug 与 Release 验收全部通过,且未执行 Git 自检或提交。 diff --git a/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md new file mode 100644 index 0000000..0d05929 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Initial_plan/AMR_轨迹规划技术路线_Agent版.md @@ -0,0 +1,1194 @@ +# AMR 非结构化道路轨迹规划技术路线(面向 Agent) + +## 1. 项目目标 + +开发一套面向非结构化道路四舵轮 AMR 的轨迹规划系统。 + +第一阶段采用车式运动模型,具体边界如下: + +- 支持前进、倒车和换向; +- 暂不支持蟹行、纯横移和原地旋转; +- 车体航向始终表示车头朝向; +- 规划结果必须满足完整车体无碰撞、最大曲率约束和目标位姿要求; +- 最终输出可供底盘控制器跟踪的带时间轨迹。 + +整体技术路线分为四个主要阶段: + +```mermaid +flowchart LR + A[输入与地图构建] --> B[Hybrid A* 粗路径规划] + B --> C[路径平滑与可行性处理] + C --> D[SQP 轨迹优化] + D --> E[输出可跟踪轨迹] +``` + +--- + +## 2. 总体模块划分 + +```text +Module 1:输入与地图构建 +Module 2:Hybrid A* 粗路径规划 +Module 3:路径平滑与可行性处理 +Module 4:SQP 轨迹优化 +Module 5:轨迹输出与底盘接口 +``` + +各模块必须通过明确的数据结构传递结果,不允许后级模块直接读取前级模块内部状态。 + +--- + +# 3. Module 1:输入与地图构建 + +## 3.1 模块目标 + +根据起点、终点、车辆参数和环境障碍物信息,构建一张供全局搜索使用的任务级二维栅格地图。 + +地图范围至少覆盖: + +- AMR 起点; +- 目标位置; +- 起点与目标之间的规划区域; +- 周围必要的安全边界。 + +当前阶段继续使用已有 `GridMapData` 作为地图数据来源,并通过适配器转换为 Hybrid A* 使用的规划地图。 + +--- + +## 3.2 输入数据 + +### 3.2.1 起点位姿 + +```text +StartPose: + X + Y + Heading + StartDirection + StartVehicleCurvature +``` + +字段定义: + +- `X、Y`:AMR 几何中心在世界坐标系中的位置; +- `Heading`:车头航向; +- `StartDirection`:`Forward`、`Reverse` 或 `Both`; +- `StartVehicleCurvature`:车辆当前转向曲率,未提供时默认为 0。 + +--- + +### 3.2.2 终点位姿 + +```text +GoalPose: + X + Y + Heading + GoalDirectionConstraint +``` + +目标进入方向约束: + +```text +Any +Forward +Reverse +``` + +其中: + +- `GoalHeading` 始终表示目标车头朝向; +- `GoalDirectionConstraint` 只约束最后一个连续运动段的运动方向。 + +--- + +### 3.2.3 车辆参数 + +```text +VehicleParameters: + VehicleLength + VehicleWidth + MaxVehicleCurvature + MinTurningRadius +``` + +第一阶段车辆参考点统一采用 AMR 几何中心。 + +--- + +### 3.2.4 环境障碍物 + +当前已接入: + +```text +TwoLegDetect 两腿检测结果 +``` + +后续扩展: + +```text +点云障碍物 +激光雷达障碍物 +静态障碍物 +动态障碍物 +禁行区域 +``` + +--- + +### 3.2.5 地图配置 + +```text +MapConfiguration: + GridResolution + MapMarginX + MapMarginY + ObstacleInflation + MaximumCellCount +``` + +--- + +## 3.3 当前地图构建逻辑 + +```mermaid +flowchart TD + A[读取 AMR 位姿与车身尺寸] --> B[读取目标位置与地图参数] + B --> C[计算任务级地图边界] + C --> D[创建 GridMapData] + D --> E[读取 TwoLegDetect 障碍物] + E --> F[车体系坐标转世界坐标] + F --> G[圆形障碍物栅格化] + G --> H[输出环境占据栅格] +``` + +当前地图边界由起点与目标位置共同决定: + +```text +XMin = min(StartX, GoalX) - MapMarginX +XMax = max(StartX, GoalX) + MapMarginX + +YMin = min(StartY, GoalY) - MapMarginY +YMax = max(StartY, GoalY) + MapMarginY +``` + +栅格尺寸: + +```text +Cols = ceil((XMax - XMin) / Resolution) +Rows = ceil((YMax - YMin) / Resolution) +``` + +--- + +## 3.4 当前地图适配要求 + +现有地图格式为: + +```text +单位:mm +数组:Cells[row, col] +状态:0 = 未标记占据,1 = Occupied +``` + +Hybrid A* 内部统一使用: + +```text +单位:m +角度:rad +曲率:1/m +``` + +因此需要增加: + +```text +PlanningMapAdapter +``` + +适配流程: + +```mermaid +flowchart TD + A[GridMapData] --> B[校验边界、尺寸与分辨率] + B --> C[深拷贝 Occupancy] + C --> D[毫米转换为米] + D --> E[生成障碍物距离场] + E --> F[输出 PlanningGridMap] +``` + +--- + +## 3.5 地图构建阶段的关键规则 + +### 3.5.1 规划地图不得包含 AMR 自身占据层 + +AMR 自身不能写入环境占据地图,否则 Hybrid A* 起点会直接被判定为碰撞。 + +正确关系: + +```text +环境地图: + 只包含外部障碍物 + +AMR 自身: + 通过 VehiclePose、VehicleLength、VehicleWidth 单独描述 +``` + +--- + +### 3.5.2 障碍物膨胀量必须明确 + +建议将: + +```text +SafetyMarginMm +``` + +改为: + +```text +ObstacleInflationMm +``` + +其唯一含义是: + +> 障碍物写入地图前向外膨胀的距离。 + +必须避免: + +```text +障碍物已经膨胀 ++ +车辆碰撞矩形再次增加同样余量 +``` + +--- + +### 3.5.3 区分地图创建成功与地图可用于规划 + +```text +Succeeded: + 地图数据结构是否创建成功 + +PlanningReady: + 当前地图是否满足规划输入要求 + +PlanningBlockReason: + 地图为何不能用于规划 +``` + +例如: + +```text +TwoLegDetect 正常: + Succeeded = true + PlanningReady = true + +TwoLegDetect 无结果: + Succeeded = true + PlanningReady = false +``` + +--- + +## 3.6 地图输出 + +```text +PlanningGridMap: + XMin + XMax + YMin + YMax + Resolution + Rows + Cols + Occupancy[row, col] + ObstacleDistance[row, col] + ObstacleInflation + InputSource + PlanningReady + MapVersion +``` + +当前第一版状态定义: + +```text +Occupancy = 0: + 当前地图层未标记为占据 + +Occupancy = 1: + 占据 + +地图外部: + 按占据处理 +``` + +--- + +# 4. Module 2:Hybrid A* 粗路径规划 + +## 4.1 模块目标 + +在任务级栅格地图中搜索一条: + +- 从起点到目标附近; +- 完整车体无碰撞; +- 满足最大曲率限制; +- 支持前进、倒车和换向; +- 具有正确绕障拓扑; +- 可作为后续平滑和 SQP 优化初值; + +的空间粗路径。 + +Hybrid A* 负责决定: + +```text +从哪里走 +绕障碍物哪一侧走 +什么时候前进 +什么时候倒车 +在哪里换向 +以什么车头航向接近目标 +``` + +Hybrid A* 不负责: + +```text +速度 +加速度 +角速度 +时间戳 +舵轮角度 +轮速 +``` + +--- + +## 4.2 搜索状态 + +连续状态: + +```text +State: + x + y + theta + direction + curvatureIndex +``` + +字段定义: + +- `x、y`:AMR 几何中心位置; +- `theta`:车头航向; +- `direction`:`Forward` 或 `Reverse`; +- `curvatureIndex`:当前离散车辆曲率等级。 + +Closed Set 键: + +```text +GridKey: + ix + iy + iHeading + direction + curvatureIndex +``` + +--- + +## 4.3 运动模型 + +定义: + +```text +directionSign = +1:Forward +directionSign = -1:Reverse +``` + +运动积分: + +```text +dx/ds = directionSign × cos(theta) +dy/ds = directionSign × sin(theta) +dtheta/ds = directionSign × VehicleCurvature +``` + +其中: + +```text +Heading: + 始终表示车头朝向 + +ArcLength: + 始终非负并单调增加 +``` + +--- + +## 4.4 运动原语 + +第一版离散曲率集合: + +```text +-kappaMax +-0.5 × kappaMax +0 ++0.5 × kappaMax ++kappaMax +``` + +每个曲率均支持: + +```text +Forward +Reverse +``` + +第一版建议参数: + +```text +PrimitiveLength = 0.50 m +IntegrationStep = 0.05 m +``` + +每条原语按 0.05 m 步长积分,并在每个内部积分点执行完整车体碰撞检测。 + +--- + +## 4.5 搜索约束 + +每个候选原语必须检查: + +```text +地图边界 +Occupied 栅格 +完整车辆矩形碰撞 +安全净空 +最大车辆曲率 +允许的运动方向 +曲率等级切换规则 +目标进入方向 +``` + +第一阶段暂不直接约束: + +```text +速度 +加速度 +曲率时间变化率 +舵轮转向速度 +``` + +但需要通过代价项减少极端曲率跳变。 + +--- + +## 4.6 搜索代价 + +```text +g = + 路径长度代价 + + 倒车代价 + + 换向代价 + + 曲率代价 + + 曲率变化代价 + + 障碍物距离代价 +``` + +启发函数: + +```text +h = max(h2D, hRS) +``` + +其中: + +```text +h2D: + 基于二维 Dijkstra 的绕障距离 + +hRS: + 基于 Reeds–Shepp 模型的非完整约束距离 +``` + +节点排序: + +```text +f = g + HeuristicWeight × h +``` + +--- + +## 4.7 终点附近 Reeds–Shepp 连接 + +```mermaid +flowchart TD + A[当前节点接近目标] --> B[生成 Reeds-Shepp 候选] + B --> C[按 0.05 m 采样] + C --> D[完整车体碰撞检查] + D --> E[最大曲率与目标方向检查] + E -->|成功| F[精确连接目标] + E -->|失败| G[继续 Hybrid A* 搜索] +``` + +支持模式: + +```text +Disabled +Opportunistic +RequiredNearGoal +``` + +停车、钻入和精确泊入任务优先使用: + +```text +RequiredNearGoal +``` + +--- + +## 4.8 Hybrid A* 输出 + +```text +CoarsePathPoint: + X + Y + Heading + UnwrappedHeading + ArcLength + Direction + VehicleCurvature + GeometricCurvature + BodyClearance + IsGearSwitchPoint + Source +``` + +路径按运动方向拆分: + +```text +PathSegment: + SegmentIndex + Direction + StartIndex + EndIndex + StartsAtGearSwitch + EndsAtGearSwitch +``` + +--- + +# 5. Module 3:路径平滑与可行性处理 + +## 5.1 模块目标 + +消除 Hybrid A* 粗路径中的离散折线和不连续曲率,生成更平滑的空间参考路径。 + +该阶段只优化几何形状,不进行时间参数化。 + +不得改变: + +```text +绕障侧 +前进与倒车顺序 +换向次数 +方向段拓扑 +目标进入方向 +``` + +--- + +## 5.2 处理流程 + +```mermaid +flowchart TD + A[Hybrid A* 稠密粗路径] --> B[删除重复点] + B --> C[按 Forward/Reverse 分段] + C --> D[保留关键点] + D --> E[按弧长重采样] + E --> F[几何平滑] + F --> G[完整可行性检查] + G --> H[平滑空间路径] +``` + +--- + +## 5.3 必须保留的关键点 + +```text +起点 +终点 +换向点 +曲率等级切换点 +直线与弯道切换点 +Reeds–Shepp 连接点 +局部最小净空点 +明显航向变化点 +``` + +禁止跨换向点进行单条曲线拟合。 + +--- + +## 5.4 平滑方法 + +### 5.4.1 贝塞尔曲线 + +适用于: + +```text +局部短转角 +折线拐点快速平滑 +局部连接 +``` + +特点: + +```text +实现简单 +计算速度快 +局部形状易控制 +``` + +--- + +### 5.4.2 五次多项式 + +适用于: + +```text +末端短距离连接 +具有明确起终点位置、航向和曲率的局部段 +``` + +特点: + +```text +可同时约束位置、航向和曲率 +适合末端姿态调整 +``` + +--- + +### 5.4.3 B 样条 + +适用于: + +```text +较长的普通路径段 +多点连续平滑 +降低局部折线感 +``` + +建议采用三次 B 样条作为第一版普通路径段的主平滑方法。 + +--- + +## 5.5 可行性检查 + +平滑后的路径必须重新检查: + +```text +完整车体碰撞 +地图边界 +Unknown / Occupied +最小安全净空 +最大车辆曲率 +路径位置连续性 +路径切线连续性 +换向结构 +目标进入方向 +``` + +若平滑失败: + +```text +局部重新调整 + ↓ +局部 QP 兜底 + ↓ +仍失败则回退原始可行粗路径 +``` + +--- + +## 5.6 输出 + +```text +SmoothedPathPoint: + X + Y + Heading + ArcLength + Direction + VehicleCurvature + GeometricCurvature + BodyClearance + Source +``` + +该输出仍然不包含: + +```text +速度 +加速度 +时间戳 +``` + +--- + +# 6. Module 4:SQP 轨迹优化 + +## 6.1 模块目标 + +以平滑空间路径作为参考线,通过 Frenet 坐标系和 SQP 优化,联合完成: + +- 横向几何调整; +- 曲率优化; +- 纵向速度规划; +- 加速度约束; +- 换向停车约束; +- 终点精确收敛; +- 时间参数化。 + +最终生成可供车辆控制器跟踪的带时间轨迹。 + +--- + +## 6.2 Frenet 坐标转换 + +将平滑路径作为参考线,建立 Frenet 坐标系: + +```text +s: + 沿参考线的累计弧长 + +l: + 相对于参考线的横向偏移 +``` + +转换后,将原二维路径问题分解为: + +```text +横向 l-s 问题 +纵向 s-t 问题 +``` + +--- + +## 6.3 横向 l-s 优化 + +主要优化变量: + +```text +横向偏移 l +横向一阶导数 dl/ds +横向二阶导数 d²l/ds² +曲率 +曲率变化 +``` + +优化目标: + +```text +尽量贴近参考线 +避开障碍物 +保持曲率平滑 +满足最大曲率 +满足安全走廊约束 +精确到达目标横向位置 +``` + +--- + +## 6.4 纵向 s-t 优化 + +主要优化变量: + +```text +s(t) +速度 v(t) +加速度 a(t) +加加速度 jerk(t) +``` + +优化目标: + +```text +满足速度上限 +满足加速度约束 +满足减速度约束 +提高运动平顺性 +减少总时间 +在换向点停车 +在终点停车 +``` + +--- + +## 6.5 SQP 联合优化 + +SQP 负责迭代求解: + +```text +路径位置 +航向 +曲率 +速度 +加速度 +角速度 +时间戳 +``` + +约束至少包括: + +```text +障碍物安全约束 +车辆曲率约束 +速度约束 +加速度约束 +角速度约束 +换向点速度为 0 +终点位置约束 +终点航向约束 +终点速度约束 +``` + +SQP 不负责: + +```text +重新决定绕障侧 +凭空新增换向 +改变 Hybrid A* 已确定的拓扑结构 +``` + +--- + +## 6.6 SQP 输出 + +```text +TrajectoryPoint: + X + Y + Heading + Time + ArcLength + Direction + Velocity + Acceleration + AngularVelocity + VehicleCurvature + GeometricCurvature +``` + +该轨迹可以直接提供给后续底盘控制与轨迹跟踪模块。 + +--- + +# 7. Module 5:轨迹输出与底盘接口 + +## 7.1 输出内容 + +```text +TrajectoryResult: + PlanningStatus + GoalReachType + PathProcessingResult + TrajectoryPoints + DirectionSegments + Diagnostics +``` + +--- + +## 7.2 底盘控制前的处理 + +轨迹规划器不直接输出四轮舵角和轮速。 + +底盘控制层负责: + +```text +轨迹点 + ↓ +车辆状态误差计算 + ↓ +轨迹跟踪控制 + ↓ +底盘运动学逆解 + ↓ +四轮舵角与轮速 +``` + +--- + +# 8. Agent 开发顺序 + +## 阶段 M0:数据结构与公共工具 + +实现: + +```text +Pose2D +VehicleParameters +GoalPose +GridMapData Adapter +角度归一化 +坐标变换 +单位转换 +``` + +完成标准: + +```text +单元测试通过 +毫米与米转换无误 +航向角定义统一 +``` + +--- + +## 阶段 M1:地图适配与碰撞检测 + +实现: + +```text +PlanningMapAdapter +PlanningGridMap +距离场 +旋转矩形碰撞检测 +地图边界检测 +``` + +完成标准: + +```text +AMR 自身不会出现在环境占据图中 +不同航向下完整车体碰撞检测正确 +``` + +--- + +## 阶段 M2:最小 Hybrid A* + +实现: + +```text +只前进 +无障碍 +恒曲率运动原语 +基础启发函数 +``` + +完成标准: + +```text +稳定从起点搜索到目标 +``` + +--- + +## 阶段 M3:障碍物、倒车和换向 + +实现: + +```text +Dijkstra 启发 +Forward / Reverse +换向代价 +方向分段 +``` + +完成标准: + +```text +单障碍绕行成功 +需要倒车的场景成功 +换向点正确输出 +``` + +--- + +## 阶段 M4:目标终止与 Reeds–Shepp + +实现: + +```text +目标位置容差 +目标航向容差 +目标进入方向 +Reeds–Shepp 三种模式 +``` + +完成标准: + +```text +普通目标和狭窄目标场景均能正确终止 +``` + +--- + +## 阶段 M5:路径后处理 + +实现: + +```text +父节点回溯 +内部积分点恢复 +关键点保留 +弧长重采样 +``` + +完成标准: + +```text +路径无断点 +换向结构明确 +方向段正确 +``` + +--- + +## 阶段 M6:平滑与可行性检查 + +实现: + +```text +B 样条 +贝塞尔曲线 +五次多项式 +完整碰撞复核 +失败回退 +``` + +完成标准: + +```text +路径曲率明显改善 +不改变原有拓扑 +平滑失败可回退 +``` + +--- + +## 阶段 M7:SQP 轨迹优化 + +实现: + +```text +Frenet 坐标转换 +l-s 优化 +s-t 优化 +速度和加速度约束 +换向停车约束 +终点精确约束 +``` + +完成标准: + +```text +输出带时间的可跟踪轨迹 +终点位置与航向满足要求 +换向点速度为 0 +``` + +--- + +## 阶段 M8:性能优化 + +在前述流程完整跑通后再开展: + +```text +性能剖析 +Open List 优化 +Closed Set 优化 +节点内存优化 +地图裁剪 +预计算运动原语 +预计算碰撞模板 +启发图复用 +路径热启动 +增量规划 +``` + +--- + +# 9. 核心技术边界 + +## Hybrid A* 负责 + +```text +拓扑 +绕障侧 +前进与倒车结构 +换向位置 +基本运动学可行性 +粗目标位姿接近 +``` + +## 路径平滑负责 + +```text +消除折线 +改善局部曲率 +保持拓扑不变 +输出连续空间参考路径 +``` + +## SQP 负责 + +```text +终点精确收敛 +速度规划 +加速度规划 +角速度规划 +时间参数化 +动态可跟踪性 +``` + +## 底盘控制负责 + +```text +四轮舵角 +四轮轮速 +轨迹跟踪 +执行器控制 +``` + +--- + +# 10. 最终技术路线总结 + +```text +起点位姿 + 目标位姿 + 车辆参数 + 障碍物数据 + ↓ +构建任务级环境占据地图 + ↓ +通过 PlanningMapAdapter 转换为规划地图 + ↓ +Hybrid A* 搜索满足运动学约束的粗路径 + ↓ +Reeds–Shepp 处理终点位姿连接 + ↓ +回溯、重采样和路径方向分段 + ↓ +B 样条 / 贝塞尔 / 五次多项式进行几何平滑 + ↓ +重新执行完整车体碰撞和曲率检查 + ↓ +建立 Frenet 参考坐标系 + ↓ +横向 l-s 优化路径形状和曲率 + ↓ +纵向 s-t 优化速度、加速度和停车决策 + ↓ +SQP 联合求解可跟踪轨迹 + ↓ +输出位置、航向、速度、加速度、角速度和时间戳 + ↓ +底盘控制器进行四轮舵角和轮速逆解 +``` + +该技术路线的核心原则是: + +> Hybrid A* 解决“怎么走”,路径平滑解决“走得是否顺”,SQP 解决“以什么速度和时间走”,底盘控制解决“四个舵轮和车轮如何执行”。 diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs new file mode 100644 index 0000000..f4097e3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs @@ -0,0 +1,92 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 建图阶段使用的可变环境占据栅格。 +/// +/// 单位:边界、世界查询和栅格边长均为 mm。 +/// 注意:只有 可以写入占据状态;规划阶段应改用不可变的 。 +/// +public sealed class EnvironmentGridMap +{ + private readonly byte[] _cells; + private int _occupiedCount; + + /// + /// 创建空的环境占据栅格。 + /// + /// 参数:bounds 为左闭右开的世界边界,单位 mm;resolutionMm 为格边长,单位 mm。 + /// 返回:无;边界为空或分辨率不合法时抛出异常。 + /// + public EnvironmentGridMap(MapBoundsMm bounds, float resolutionMm) + { + if (bounds == null) throw new ArgumentNullException(nameof(bounds)); + bounds.GetDimensions(resolutionMm, out int rows, out int cols); + Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; + _cells = new byte[checked(rows * cols)]; + } + + /// 地图世界边界,单位 mm,采用左闭右开规则。 + public MapBoundsMm Bounds { get; } + /// 单个栅格边长,单位 mm。 + public float ResolutionMm { get; } + /// 栅格行数,Y 方向从下限向上递增。 + public int Rows { get; } + /// 栅格列数,X 方向从下限向右递增。 + public int Cols { get; } + /// 当前已被标记为障碍的格数。 + public int OccupiedCount { get { return _occupiedCount; } } + + /// 判断行列索引是否有效。参数 row、col 分别为从零开始的行和列;有效时返回 true。 + public bool IsInBounds(int row, int col) { return row >= 0 && row < Rows && col >= 0 && col < Cols; } + /// 判断世界坐标是否位于地图内。参数 xMm、yMm 单位为 mm;上边界与右边界返回 false。 + public bool IsWorldInBounds(float xMm, float yMm) { return Bounds.Contains(xMm, yMm); } + + /// + /// 将世界坐标转换为栅格索引。 + /// + /// 参数:xMm、yMm 为世界坐标,单位 mm;row、col 为输出索引。 + /// 返回:坐标在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 + /// + public bool TryWorldToGrid(float xMm, float yMm, out int row, out int col) + { + row = -1; col = -1; + if (!IsWorldInBounds(xMm, yMm)) return false; + col = (int)Math.Floor(((double)xMm - Bounds.XMin) / ResolutionMm); + row = (int)Math.Floor(((double)yMm - Bounds.YMin) / ResolutionMm); + return IsInBounds(row, col); + } + + /// 查询栅格是否占据。越界索引按障碍处理,返回 true。 + public bool IsOccupied(int row, int col) { return !IsInBounds(row, col) || _cells[row * Cols + col] != 0; } + /// 按世界坐标查询占据状态。参数 xMm、yMm 单位为 mm;坐标越界时保守地返回 true。 + public bool IsOccupiedWorld(float xMm, float yMm) + { + return !TryWorldToGrid(xMm, yMm, out int row, out int col) || IsOccupied(row, col); + } + + /// + /// 获取一个栅格的世界坐标范围。 + /// + /// 参数:row、col 为有效索引;xMin、xMax、yMin、yMax 为输出边界,单位 mm。 + /// 返回:无;索引越界时抛出 。 + /// + public void GetCellBounds(int row, int col, out float xMin, out float xMax, out float yMin, out float yMax) + { + if (!IsInBounds(row, col)) throw new ArgumentOutOfRangeException(); + xMin = Bounds.XMin + col * ResolutionMm; + yMin = Bounds.YMin + row * ResolutionMm; + xMax = Math.Min(Bounds.XMax, xMin + ResolutionMm); + yMax = Math.Min(Bounds.YMax, yMin + ResolutionMm); + } + + internal void MarkOccupied(int row, int col) + { + if (!IsInBounds(row, col)) return; + int index = row * Cols + col; + if (_cells[index] == 0) { _cells[index] = 1; _occupiedCount++; } + } + internal byte[] CopyCells() { return (byte[])_cells.Clone(); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs new file mode 100644 index 0000000..d084079 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 环境占据图构建结果。 +/// 返回:成功时提供可供适配的 EnvironmentGridMap;失败时提供失败原因和已处理来源状态。 +/// +public sealed class EnvironmentMapBuildResult +{ + private EnvironmentMapBuildResult(bool succeeded, EnvironmentGridMap map, IReadOnlyList sourceResults, string failureReason, PlanningOperationStopReason stopReason) + { + Succeeded = succeeded; Map = map; SourceResults = sourceResults ?? Array.Empty(); FailureReason = failureReason ?? string.Empty; StopReason = stopReason; + } + /// 构建是否成功。true 时 Map 非空;false 时读取 FailureReason。 + public bool Succeeded { get; } + /// 成功生成的构建期环境栅格;失败时为 null。 + public EnvironmentGridMap Map { get; } + /// 已尝试来源的投影结果,用于记录已应用、空或失败状态。 + public IReadOnlyList SourceResults { get; } + /// 失败原因。成功时为空字符串。 + public string FailureReason { get; } + /// 内部预算停止原因;普通构建成功或失败时为 None。 + internal PlanningOperationStopReason StopReason { get; } + /// 创建成功结果。参数 map 为已完成栅格,sourceResults 为来源投影记录。 + public static EnvironmentMapBuildResult Success(EnvironmentGridMap map, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(true, map, sourceResults, null, PlanningOperationStopReason.None); } + /// 创建失败结果。参数 reason 为诊断文本,sourceResults 可包含失败前已处理的来源。 + public static EnvironmentMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new EnvironmentMapBuildResult(false, null, sourceResults, reason, PlanningOperationStopReason.None); } + /// 创建已取消或超时结果;不发布构建期可写地图。 + internal static EnvironmentMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) + { + if (stopReason == PlanningOperationStopReason.None) throw new ArgumentOutOfRangeException(nameof(stopReason)); + return new EnvironmentMapBuildResult(false, null, sourceResults, + stopReason == PlanningOperationStopReason.Cancelled ? "地图构建已取消。" : "地图构建已超时。", stopReason); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs new file mode 100644 index 0000000..31e4c0d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 从排序后的纯障碍物快照事务性构建环境占据图。 +/// +/// 注意:必需来源返回不可用或无效状态时,构建整体失败;可选来源仅记录其状态并继续构建。 +/// +public sealed class EnvironmentMapBuilder +{ + /// + /// 投影所有障碍物来源并栅格化为环境地图。 + /// + /// 参数:request 包含 mm 世界边界、分辨率和来源列表;每个来源 ID 必须唯一且版本非负。 + /// 返回:成功时包含 和全部来源状态;必需来源失败时返回失败结果而不产生可用地图。 + /// + public EnvironmentMapBuildResult Build(MapBuildRequest request) + { + return Build(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); + } + + /// 使用共享预算投影来源并栅格化;停止时不发布可写环境地图。 + internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget) + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, null); + if (request == null || request.Bounds == null) return EnvironmentMapBuildResult.Failure("Map request and bounds are required.", null); + if (request.ObstacleSources == null) return EnvironmentMapBuildResult.Failure("Obstacle source collection is required.", null); + var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); + var results = new List(); + string previousId = null; + for (int i = 0; i < sources.Length; i++) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); + IMapObstacleSource source = sources[i]; + if (source == null || string.IsNullOrWhiteSpace(source.SourceId) || source.SourceVersion < 0) + return EnvironmentMapBuildResult.Failure("Each source needs a non-empty id and non-negative version.", results); + if (string.Equals(previousId, source.SourceId, StringComparison.Ordinal)) + return EnvironmentMapBuildResult.Failure("Obstacle source ids must be unique.", results); + previousId = source.SourceId; + ObstacleProjectionResult result; + try { result = source.ProjectToWorld() ?? ObstacleProjectionResult.Invalid("Source returned no projection result."); } + catch (Exception exception) { result = ObstacleProjectionResult.Invalid(exception.Message); } + results.Add(result); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return EnvironmentMapBuildResult.Stopped(stopReason, results); + if (source.IsRequired && (result.Status == ObstacleSourceStatus.Invalid || result.Status == ObstacleSourceStatus.Unavailable)) + return EnvironmentMapBuildResult.Failure("A required obstacle source failed: " + source.SourceId, results); + } + var map = new EnvironmentGridMap(request.Bounds, request.ResolutionMm); + for (int i = 0; i < results.Count; i++) + if (results[i].Status == ObstacleSourceStatus.Applied) + for (int j = 0; j < results[i].Obstacles.Count; j++) + { + if (!MapObstacleRasterizer.TryRasterize(map, results[i].Obstacles[j], budget, out stopReason)) + return EnvironmentMapBuildResult.Stopped(stopReason, results); + } + return EnvironmentMapBuildResult.Success(map, results); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs new file mode 100644 index 0000000..96c707b --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs @@ -0,0 +1,86 @@ +using System; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 有限的世界地图边界。 +/// 单位:mm;范围采用左闭右开 [XMin, XMax) × [YMin, YMax)。 +/// +public sealed class MapBoundsMm : IEquatable +{ + /// 单张地图允许的最大栅格数,超过该值会拒绝创建地图。 + public const int MaximumCellCount = 4000000; + + /// + /// 创建地图世界边界。 + /// + /// 参数: + /// - xMin、xMax:世界 X 轴下限和上限,单位 mm,且 xMax 必须大于 xMin。 + /// - yMin、yMax:世界 Y 轴下限和上限,单位 mm,且 yMax 必须大于 yMin。 + /// + /// 注意:边界采用左闭右开规则,上限坐标不属于地图。 + /// + public MapBoundsMm(float xMin, float xMax, float yMin, float yMax) + { + if (!NumericGuard.IsFinite(xMin) || !NumericGuard.IsFinite(xMax) || + !NumericGuard.IsFinite(yMin) || !NumericGuard.IsFinite(yMax) || + xMax <= xMin || yMax <= yMin) + throw new ArgumentOutOfRangeException(nameof(xMax), "Map bounds must be finite and non-degenerate."); + XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; + } + + /// 世界 X 轴下限,单位 mm,包含在地图内。 + public float XMin { get; } + /// 世界 X 轴上限,单位 mm,不包含在地图内。 + public float XMax { get; } + /// 世界 Y 轴下限,单位 mm,包含在地图内。 + public float YMin { get; } + /// 世界 Y 轴上限,单位 mm,不包含在地图内。 + public float YMax { get; } + + /// + /// 判断世界坐标是否属于地图边界。 + /// + /// 参数:xMm、yMm 为世界坐标,单位 mm。 + /// 返回:坐标位于 [XMin, XMax) × [YMin, YMax) 时为 true,否则为 false。 + /// + public bool Contains(float xMm, float yMm) + { + return xMm >= XMin && xMm < XMax && yMm >= YMin && yMm < YMax; + } + + /// + /// 根据栅格分辨率计算行列数。 + /// + /// 参数:resolutionMm 为每个方格的边长,单位 mm,取值必须在 [20, 200];rows、cols 为输出行数和列数。 + /// 返回:无;当分辨率无效或总格数超过 时抛出异常。 + /// + public void GetDimensions(float resolutionMm, out int rows, out int cols) + { + if (!NumericGuard.IsInRange(resolutionMm, 20f, 200f)) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "ResolutionMm must be within [20, 200]."); + double columnCount = Math.Ceiling(((double)XMax - XMin) / resolutionMm); + double rowCount = Math.Ceiling(((double)YMax - YMin) / resolutionMm); + if (columnCount > int.MaxValue || rowCount > int.MaxValue || columnCount <= 0d || rowCount <= 0d) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map dimensions are invalid."); + cols = (int)columnCount; rows = (int)rowCount; + long cellCount = checked((long)rows * cols); + if (cellCount > MaximumCellCount) + throw new ArgumentOutOfRangeException(nameof(resolutionMm), "Map cell count exceeds 4,000,000."); + } + + /// 比较两个边界的四个 mm 坐标是否完全相同。 + public bool Equals(MapBoundsMm other) + { + return other != null && XMin.Equals(other.XMin) && XMax.Equals(other.XMax) && + YMin.Equals(other.YMin) && YMax.Equals(other.YMax); + } + /// 比较当前边界与指定对象是否表示相同的世界范围。 + public override bool Equals(object obj) { return Equals(obj as MapBoundsMm); } + /// 返回由四个边界坐标组成的哈希值,用于缓存键比较。 + public override int GetHashCode() + { + unchecked { int hash = XMin.GetHashCode(); hash = hash * 31 + XMax.GetHashCode(); hash = hash * 31 + YMin.GetHashCode(); return hash * 31 + YMax.GetHashCode(); } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs new file mode 100644 index 0000000..5c8290e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 环境占据图构建器的输入数据。 +/// 注意:通常由 PlanningMapFactory 从公开请求转换得到,调用者无需直接使用。 +/// +public sealed class MapBuildRequest +{ + /// 环境图世界边界。单位:mm;不能为空。 + public MapBoundsMm Bounds { get; set; } + /// 环境栅格边长。单位:mm;必须满足 MapBoundsMm 的分辨率限制。 + public float ResolutionMm { get; set; } + /// 待投影的障碍物来源列表;每个来源 ID 必须唯一。 + public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs new file mode 100644 index 0000000..e9ce924 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs @@ -0,0 +1,31 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 边与世界 X/Y 轴平行的矩形障碍物,坐标单位为 mm。 +public sealed class AxisAlignedRectangleObstacle : IMapObstacle +{ + /// + /// 创建轴对齐矩形障碍物。 + /// + /// 参数:xMin、xMax、yMin、yMax 分别为矩形世界坐标边界,单位 mm。 + /// 注意:构造不校验边界顺序,请通过 判断后再使用。 + /// + public AxisAlignedRectangleObstacle(float xMin, float xMax, float yMin, float yMax) + { + XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; + } + /// 矩形世界 X 下边界,单位 mm。 + public float XMin { get; } + /// 矩形世界 X 上边界,单位 mm。 + public float XMax { get; } + /// 矩形世界 Y 下边界,单位 mm。 + public float YMin { get; } + /// 矩形世界 Y 上边界,单位 mm。 + public float YMax { get; } + /// 四个边界均为有限数且 XMax≥XMin、YMax≥YMin 时为 true;否则为 false。 + public bool IsValid + { + get { return NumericGuard.IsFinite(XMin) && NumericGuard.IsFinite(XMax) && NumericGuard.IsFinite(YMin) && NumericGuard.IsFinite(YMax) && XMax >= XMin && YMax >= YMin; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs new file mode 100644 index 0000000..55eee5f --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs @@ -0,0 +1,29 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 以世界 mm 坐标表示的圆形障碍物。 +public sealed class CircleObstacle : IMapObstacle +{ + /// + /// 创建圆形障碍物。 + /// + /// 参数:centerX、centerY 为圆心世界坐标,单位 mm;radiusMm 为半径,单位 mm。 + /// 注意:构造不抛出几何校验异常,请通过 判断后再使用。 + /// + public CircleObstacle(float centerX, float centerY, float radiusMm) + { + CenterX = centerX; CenterY = centerY; RadiusMm = radiusMm; + } + /// 圆心世界 X 坐标,单位 mm。 + public float CenterX { get; } + /// 圆心世界 Y 坐标,单位 mm。 + public float CenterY { get; } + /// 圆的半径,单位 mm。 + public float RadiusMm { get; } + /// 圆心和半径均为有限数且半径不小于零时为 true;否则为 false。 + public bool IsValid + { + get { return NumericGuard.IsFinite(CenterX) && NumericGuard.IsFinite(CenterY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs new file mode 100644 index 0000000..2377271 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs @@ -0,0 +1,12 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 世界坐标中的不可变障碍物几何。 +/// +/// 单位:所有几何坐标与尺寸均为 mm。实现类型必须能由 栅格化。 +/// +public interface IMapObstacle +{ + /// 几何数据是否有效。true 表示数值有限且尺寸满足该几何类型的约束;false 表示不得投影到地图。 + bool IsValid { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs new file mode 100644 index 0000000..44d35de --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 将障碍物几何保守投影为栅格占据状态的唯一写入入口。 +/// +/// 注意:调用方不能直接改写 ;相交或贴边的栅格均按占据处理。 +/// +public static class MapObstacleRasterizer +{ + /// + /// 将一个有效障碍物栅格化到环境地图。 + /// + /// 参数:map 为待写入的环境栅格;obstacle 为世界 mm 坐标的圆形或轴对齐矩形障碍物。 + /// 返回:无。地图或障碍物为空、障碍物无效、几何类型不受支持时抛出异常。 + /// 注意:该方法只增加占据格,不会清除已有障碍。 + /// + public static void Rasterize(EnvironmentGridMap map, IMapObstacle obstacle) + { + if (!TryRasterize(map, obstacle, PlanningOperationBudget.Unlimited(CancellationToken.None), out _)) + throw new InvalidOperationException("Unbounded rasterization unexpectedly stopped."); + } + + /// 使用共享预算将障碍物写入环境栅格;停止时返回 false 且不发布环境地图。 + internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, PlanningOperationBudget budget, + out PlanningOperationStopReason stopReason) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + if (obstacle == null || !obstacle.IsValid) throw new ArgumentException("Obstacle must be valid.", nameof(obstacle)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + int workItemCount = 0; + var circle = obstacle as CircleObstacle; + if (circle != null) return TryRasterizeCircle(map, circle, budget, ref workItemCount, out stopReason); + var rectangle = obstacle as AxisAlignedRectangleObstacle; + if (rectangle != null) return TryRasterizeRectangle(map, rectangle, budget, ref workItemCount, out stopReason); + throw new NotSupportedException("Unsupported map obstacle geometry."); + } + + private static bool TryRasterizeCircle(EnvironmentGridMap map, CircleObstacle circle, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + GetCandidateRange(map, circle.CenterX - circle.RadiusMm, circle.CenterX + circle.RadiusMm, + circle.CenterY - circle.RadiusMm, circle.CenterY + circle.RadiusMm, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + double radiusSquared = (double)circle.RadiusMm * circle.RadiusMm; + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); + double nearestX = Math.Max(xMin, Math.Min(circle.CenterX, xMax)); + double nearestY = Math.Max(yMin, Math.Min(circle.CenterY, yMax)); + double dx = circle.CenterX - nearestX; + double dy = circle.CenterY - nearestY; + if (dx * dx + dy * dy <= radiusSquared) map.MarkOccupied(row, col); + } + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static bool TryRasterizeRectangle(EnvironmentGridMap map, AxisAlignedRectangleObstacle rectangle, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + GetCandidateRange(map, rectangle.XMin, rectangle.XMax, rectangle.YMin, rectangle.YMax, + out int firstRow, out int lastRow, out int firstCol, out int lastCol); + for (int row = firstRow; row <= lastRow; row++) + for (int col = firstCol; col <= lastCol; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + map.GetCellBounds(row, col, out float xMin, out float xMax, out float yMin, out float yMax); + if (rectangle.XMax >= xMin && rectangle.XMin <= xMax && rectangle.YMax >= yMin && rectangle.YMin <= yMax) + map.MarkOccupied(row, col); + } + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static void GetCandidateRange(EnvironmentGridMap map, float xMin, float xMax, float yMin, float yMax, + out int firstRow, out int lastRow, out int firstCol, out int lastCol) + { + if (xMax < map.Bounds.XMin || xMin > map.Bounds.XMax || yMax < map.Bounds.YMin || yMin > map.Bounds.YMax) + { firstRow = 1; lastRow = 0; firstCol = 1; lastCol = 0; return; } + // Geometry is closed for conservative rasterisation. Include the cell on + // the lower side when a boundary lies exactly on a grid line. + firstCol = Clamp((int)Math.Floor(((double)xMin - map.Bounds.XMin) / map.ResolutionMm) - 1, 0, map.Cols - 1); + lastCol = Clamp((int)Math.Floor(((double)xMax - map.Bounds.XMin) / map.ResolutionMm), 0, map.Cols - 1); + firstRow = Clamp((int)Math.Floor(((double)yMin - map.Bounds.YMin) / map.ResolutionMm) - 1, 0, map.Rows - 1); + lastRow = Clamp((int)Math.Floor(((double)yMax - map.Bounds.YMin) / map.ResolutionMm), 0, map.Rows - 1); + } + private static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs new file mode 100644 index 0000000..2bdba59 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 针对行主序二值栅格计算精确欧氏距离平方的内部算法。 +internal static class EuclideanDistanceTransform +{ + /// + /// 计算每个栅格到最近障碍栅格的距离平方。 + /// + /// 参数:occupied 为行主序占据数组,非零表示障碍;rows、cols 为数组尺寸。 + /// 返回:行主序距离平方数组,单位为栅格边长的平方;不含任何 mm 或 m 换算。 + /// + public static double[] ComputeSquaredDistances(byte[] occupied, int rows, int cols) + { + if (!TryComputeSquaredDistances(occupied, rows, cols, PlanningOperationBudget.Unlimited(CancellationToken.None), + out double[] squared, out _)) + throw new InvalidOperationException("Unbounded Euclidean distance transform unexpectedly stopped."); + return squared; + } + + /// 使用共享预算计算距离平方;停止时不返回部分数组。 + internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, PlanningOperationBudget budget, + out double[] squaredDistances, out PlanningOperationStopReason stopReason) + { + if (occupied == null) throw new ArgumentNullException(nameof(occupied)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + squaredDistances = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + double noObstacleDistanceSquared = (double)rows * rows + (double)cols * cols + 1d; + var intermediate = new double[occupied.Length]; + var result = new double[occupied.Length]; + var input = new double[Math.Max(rows, cols)]; + var output = new double[Math.Max(rows, cols)]; + int workItemCount = 0; + for (int row = 0; row < rows; row++) + { + int offset = row * cols; + for (int col = 0; col < cols; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + input[col] = occupied[offset + col] == 0 ? noObstacleDistanceSquared : 0d; + } + if (!TryTransform1D(input, cols, output, budget, ref workItemCount, out stopReason)) return false; + for (int col = 0; col < cols; col++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + intermediate[offset + col] = output[col]; + } + } + for (int col = 0; col < cols; col++) + { + for (int row = 0; row < rows; row++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + input[row] = intermediate[row * cols + col]; + } + if (!TryTransform1D(input, rows, output, budget, ref workItemCount, out stopReason)) return false; + for (int row = 0; row < rows; row++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + result[row * cols + col] = output[row]; + } + } + squaredDistances = result; + stopReason = PlanningOperationStopReason.None; + return true; + } + + private static bool TryTransform1D(double[] f, int length, double[] result, PlanningOperationBudget budget, + ref int workItemCount, out PlanningOperationStopReason stopReason) + { + var locations = new int[length]; + var boundaries = new double[length + 1]; + int k = 0; + locations[0] = 0; + boundaries[0] = double.NegativeInfinity; + boundaries[1] = double.PositiveInfinity; + for (int q = 1; q < length; q++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + double intersection; + do + { + int p = locations[k]; + intersection = ((f[q] + (double)q * q) - (f[p] + (double)p * p)) / (2d * (q - p)); + if (intersection <= boundaries[k]) + { + k--; + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + } + } while (k >= 0 && intersection <= boundaries[k]); + if (k < 0) + { + k = 0; locations[0] = q; boundaries[0] = double.NegativeInfinity; boundaries[1] = double.PositiveInfinity; + } + else + { + k++; locations[k] = q; boundaries[k] = intersection; boundaries[k + 1] = double.PositiveInfinity; + } + } + k = 0; + for (int q = 0; q < length; q++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + while (boundaries[k + 1] < q) k++; + double delta = q - locations[k]; + result[q] = delta * delta + f[locations[k]]; + } + stopReason = PlanningOperationStopReason.None; + return true; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs new file mode 100644 index 0000000..385551d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 以 m 表示的障碍物净距离保守下界。 +internal sealed class ObstacleDistanceField +{ + private readonly double[] _conservativeDistances; + private ObstacleDistanceField(double[] conservativeDistances) { _conservativeDistances = conservativeDistances; } + /// + /// 从占据栅格创建距离场。 + /// + /// 参数:occupied 为行主序占据数组;rows、cols 为其尺寸;resolutionMeters 为格边长,单位 m。 + /// 返回:每个格到最近障碍物的保守净距离下界,单位 m;全空地图中的每项为正无穷。 + /// + public static ObstacleDistanceField Create(byte[] occupied, int rows, int cols, double resolutionMeters) + { + if (!TryCreate(occupied, rows, cols, resolutionMeters, PlanningOperationBudget.Unlimited(CancellationToken.None), + out ObstacleDistanceField field, out _)) + throw new InvalidOperationException("Unbounded distance-field creation unexpectedly stopped."); + return field; + } + + /// 使用共享预算创建距离场;停止时不返回部分距离数据。 + internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, + PlanningOperationBudget budget, out ObstacleDistanceField field, out PlanningOperationStopReason stopReason) + { + if (occupied == null) throw new ArgumentNullException(nameof(occupied)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + field = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + bool hasObstacle = false; + int workItemCount = 0; + for (int i = 0; i < occupied.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + if (occupied[i] != 0) { hasObstacle = true; break; } + } + var distances = new double[occupied.Length]; + if (!hasObstacle) + { + for (int i = 0; i < distances.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + distances[i] = double.PositiveInfinity; + } + field = new ObstacleDistanceField(distances); + stopReason = PlanningOperationStopReason.None; + return true; + } + if (!EuclideanDistanceTransform.TryComputeSquaredDistances(occupied, rows, cols, budget, out double[] squared, out stopReason)) + return false; + double conservativeOffset = Math.Sqrt(2d) * resolutionMeters; + for (int i = 0; i < distances.Length; i++) + { + stopReason = budget.CheckEvery(ref workItemCount); + if (stopReason != PlanningOperationStopReason.None) return false; + distances[i] = Math.Max(0d, Math.Sqrt(squared[i]) * resolutionMeters - conservativeOffset); + } + field = new ObstacleDistanceField(distances); + stopReason = PlanningOperationStopReason.None; + return true; + } + internal double[] CopyDistances() { return (double[])_conservativeDistances.Clone(); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs new file mode 100644 index 0000000..0c014ca --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs @@ -0,0 +1,88 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 供粗路径规划使用的不可变地图快照。 +/// +/// 单位:世界查询方法使用 m; 保留原始 mm 数据。 +/// 注意:世界坐标越界一律按占据处理,净距离为零。 +/// +public sealed class PlanningGridMap +{ + private readonly byte[] _occupied; + private readonly double[] _conservativeDistances; + + internal PlanningGridMap(MapBoundsMm bounds, float resolutionMm, int rows, int cols, byte[] occupied, double[] conservativeDistances, + long snapshotId, bool planningReady, string planningBlockReason, string inputFingerprint, string occupancyHash) + { + Bounds = bounds; ResolutionMm = resolutionMm; Rows = rows; Cols = cols; + _occupied = occupied; _conservativeDistances = conservativeDistances; + SnapshotId = snapshotId; PlanningReady = planningReady; PlanningBlockReason = planningBlockReason ?? string.Empty; + InputFingerprint = inputFingerprint ?? string.Empty; OccupancyHash = occupancyHash ?? string.Empty; + } + /// 源环境图的世界边界,单位 mm,采用左闭右开规则。 + public MapBoundsMm Bounds { get; } + /// 源环境图的栅格边长,单位 mm。 + public float ResolutionMm { get; } + /// 规划世界查询对应的栅格边长,单位 m。 + public double ResolutionMeters { get { return ResolutionMm / 1000d; } } + /// 栅格行数。 + public int Rows { get; } + /// 栅格列数。 + public int Cols { get; } + /// 工厂为本次返回快照分配的单调编号,用于区分不同构建结果。 + public long SnapshotId { get; } + /// 地图是否允许进入粗路径规划。true 时可直接查询;false 时应先处理 + public bool PlanningReady { get; } + /// 禁止规划的原因。 为 true 时为空字符串。 + public string PlanningBlockReason { get; } + /// 完整建图输入的稳定指纹,用于识别精确输入缓存命中。 + public string InputFingerprint { get; } + /// 占据栅格内容哈希,用于识别可复用的占据与距离数组。 + public string OccupancyHash { get; } + + /// 查询世界位置是否占据。参数 xMeters、yMeters 单位为 m;位置越界时保守地返回 true。 + public bool IsOccupiedWorld(double xMeters, double yMeters) + { + return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) || _occupied[row * Cols + col] != 0; + } + /// + /// 查询到最近障碍物的保守净距离下界。 + /// + /// 参数:xMeters、yMeters 为世界坐标,单位 m。 + /// 返回:单位 m 的非负距离下界;地图内无障碍物时为正无穷,越界时为零。 + /// + public double GetConservativeObstacleDistanceMeters(double xMeters, double yMeters) + { + return !TryWorldToGrid(xMeters, yMeters, out int row, out int col) ? 0d : _conservativeDistances[row * Cols + col]; + } + /// + /// 将规划世界坐标转换为栅格索引。 + /// + /// 参数:xMeters、yMeters 为世界坐标,单位 m;row、col 为输出索引。 + /// 返回:位置在地图内时为 true 并写入索引;否则返回 false,两个输出均为 -1。 + /// + public bool TryWorldToGrid(double xMeters, double yMeters, out int row, out int col) + { + row = -1; col = -1; + double xMm = xMeters * 1000d, yMm = yMeters * 1000d; + if (xMm < Bounds.XMin || xMm >= Bounds.XMax || yMm < Bounds.YMin || yMm >= Bounds.YMax) return false; + col = (int)Math.Floor((xMm - Bounds.XMin) / ResolutionMm); + row = (int)Math.Floor((yMm - Bounds.YMin) / ResolutionMm); + return row >= 0 && row < Rows && col >= 0 && col < Cols; + } + /// 按行列索引查询占据状态。参数从零开始;任一索引越界时返回 true。 + public bool IsOccupied(int row, int col) { return row < 0 || row >= Rows || col < 0 || col >= Cols || _occupied[row * Cols + col] != 0; } + internal byte[] CopyOccupied() { return (byte[])_occupied.Clone(); } + internal bool OccupancyEquals(PlanningGridMap other) + { + if (other == null || Rows != other.Rows || Cols != other.Cols || ResolutionMm != other.ResolutionMm || !Bounds.Equals(other.Bounds) || _occupied.Length != other._occupied.Length) return false; + for (int i = 0; i < _occupied.Length; i++) if (_occupied[i] != other._occupied[i]) return false; + return true; + } + internal PlanningGridMap WithMetadata(long snapshotId, bool planningReady, string blockReason, string inputFingerprint, string occupancyHash) + { + return new PlanningGridMap(Bounds, ResolutionMm, Rows, Cols, _occupied, _conservativeDistances, snapshotId, planningReady, blockReason, inputFingerprint, occupancyHash); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs new file mode 100644 index 0000000..3c292d9 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将建图阶段的 mm 环境栅格适配为规划阶段的不可变 m 查询快照。 +public static class PlanningMapAdapter +{ + /// + /// 创建规划地图快照及其保守距离场。 + /// + /// 参数:environmentMap 为已经完成障碍物栅格化的环境图,坐标与分辨率单位均为 mm。 + /// 返回:不可变的 ;其世界查询使用 m,初始元数据由工厂随后分配。 + /// + public static PlanningGridMap Create(EnvironmentGridMap environmentMap) + { + if (!TryCreate(environmentMap, PlanningOperationBudget.Unlimited(CancellationToken.None), out PlanningGridMap map, out _)) + throw new InvalidOperationException("Unbounded planning-map adaptation unexpectedly stopped."); + return map; + } + + /// 使用共享预算创建规划快照;停止时不返回部分地图。 + internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, + out PlanningGridMap map, out PlanningOperationStopReason stopReason) + { + if (environmentMap == null) throw new ArgumentNullException(nameof(environmentMap)); + if (budget == null) throw new ArgumentNullException(nameof(budget)); + map = null; + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + byte[] occupied = environmentMap.CopyCells(); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return false; + if (!ObstacleDistanceField.TryCreate(occupied, environmentMap.Rows, environmentMap.Cols, + environmentMap.ResolutionMm / 1000d, budget, out ObstacleDistanceField field, out stopReason)) + return false; + map = new PlanningGridMap(environmentMap.Bounds, environmentMap.ResolutionMm, environmentMap.Rows, environmentMap.Cols, + occupied, field.CopyDistances(), 0, false, "Map metadata has not been assigned.", string.Empty, string.Empty); + stopReason = budget.GetStopReason(); + if (stopReason == PlanningOperationStopReason.None) return true; + map = null; + return false; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs new file mode 100644 index 0000000..06fe232 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 线程安全、容量为四的 LRU 缓存,分别复用精确输入结果和不可变占据数组。 +internal sealed class PlanningMapCache +{ + private const int Capacity = 4; + private readonly object _gate = new object(); + private readonly LinkedList _inputs = new LinkedList(); + private readonly LinkedList _occupancies = new LinkedList(); + + /// 按完整输入描述查询缓存。命中时返回原始快照与来源结果,并提升其最近使用顺序。 + public bool TryGetInput(string descriptor, out PlanningGridMap map, out IReadOnlyList sourceResults) + { + lock (_gate) + { + for (var node = _inputs.First; node != null; node = node.Next) + if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) + { map = node.Value.Map; sourceResults = node.Value.SourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return true; } + map = null; sourceResults = null; return false; + } + } + + /// 按占据哈希及逐格比较查询缓存。命中时返回共享数组的规范快照,供工厂创建新的元数据快照。 + public bool TryGetOccupancy(string hash, PlanningGridMap candidate, out PlanningGridMap canonical) + { + lock (_gate) + { + for (var node = _occupancies.First; node != null; node = node.Next) + if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(candidate)) + { canonical = node.Value.Map; _occupancies.Remove(node); _occupancies.AddFirst(node); return true; } + canonical = null; return false; + } + } + + /// 写入或更新精确输入缓存。参数 descriptor 为完整输入键,map 为不可变快照,sourceResults 为对应来源结果。 + public void AddInput(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) + { + lock (_gate) + { + for (var node = _inputs.First; node != null; node = node.Next) + if (string.Equals(node.Value.Descriptor, descriptor, StringComparison.Ordinal)) { node.Value.Map = map; node.Value.SourceResults = sourceResults; _inputs.Remove(node); _inputs.AddFirst(node); return; } + _inputs.AddFirst(new InputEntry(descriptor, map, sourceResults)); + while (_inputs.Count > Capacity) _inputs.RemoveLast(); + } + } + + /// 写入占据缓存。参数 hash 为占据内容哈希,map 为包含可复用占据与距离数组的快照。 + public void AddOccupancy(string hash, PlanningGridMap map) + { + lock (_gate) + { + for (var node = _occupancies.First; node != null; node = node.Next) + if (string.Equals(node.Value.Hash, hash, StringComparison.Ordinal) && node.Value.Map.OccupancyEquals(map)) + { _occupancies.Remove(node); _occupancies.AddFirst(node); return; } + _occupancies.AddFirst(new OccupancyEntry(hash, map)); + while (_occupancies.Count > Capacity) _occupancies.RemoveLast(); + } + } + + private sealed class InputEntry { public InputEntry(string descriptor, PlanningGridMap map, IReadOnlyList sourceResults) { Descriptor = descriptor; Map = map; SourceResults = sourceResults; } public string Descriptor { get; } public PlanningGridMap Map { get; set; } public IReadOnlyList SourceResults { get; set; } } + private sealed class OccupancyEntry { public OccupancyEntry(string hash, PlanningGridMap map) { Hash = hash; Map = map; } public string Hash { get; } public PlanningGridMap Map { get; } } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs new file mode 100644 index 0000000..a778d3b --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 规划地图创建的终止状态。 +public enum PlanningMapBuildStatus +{ + /// 已成功创建可用的不可变地图快照。 + Success, + /// 输入、来源或地图构建失败。 + Failed, + /// 调用方在地图创建期间取消了操作。 + Cancelled, + /// 地图创建消耗了整次规划操作的总超时预算。 + TimedOut, +} + +/// 本次地图创建使用的缓存层级,反映快照或占据数组的复用方式。 +public enum PlanningMapCacheHit +{ + /// 未命中缓存;本次重新创建了占据图和距离场。 + None, + /// 完整输入命中;返回与上次完全相同的不可变地图对象。 + Input, + /// 占据内容命中;复用占据和距离数组,但生成新的快照元数据。 + Occupancy, +} + +/// +/// 规划地图创建的最终结果。 +/// +/// 返回: +/// - Succeeded 为 true 时 Map 可用;false 时通过 FailureReason 获取原因。 +/// - SourceResults 始终保留已处理来源的投影状态,便于诊断。 +/// +public sealed class PlanningMapBuildResult +{ + private PlanningMapBuildResult(PlanningMapBuildStatus status, string failureReason, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit, PlanningGridMap map) + { + Status = status; FailureReason = failureReason ?? string.Empty; SourceResults = sourceResults ?? Array.Empty(); CacheHit = cacheHit; Map = map; + } + /// 本次建图的显式终止状态;取消和超时不提供地图快照。 + public PlanningMapBuildStatus Status { get; } + /// 本次建图是否成功。true 表示 Status 为 Success 且 Map 不为空;false 时读取 FailureReason。 + public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } + /// 建图失败的可读诊断。成功时为空字符串。 + public string FailureReason { get; } + /// 每个障碍物来源的投影结果,按建图器排序后的来源顺序排列。 + public IReadOnlyList SourceResults { get; } + /// 本次调用的缓存复用层级,用于性能诊断,不影响地图正确性。 + public PlanningMapCacheHit CacheHit { get; } + /// 成功时返回的不可变规划地图;失败时为 null。 + public PlanningGridMap Map { get; } + internal static PlanningMapBuildResult Success(PlanningGridMap map, IReadOnlyList sourceResults, PlanningMapCacheHit cacheHit) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Success, null, sourceResults, cacheHit, map); } + internal static PlanningMapBuildResult Failure(string reason, IReadOnlyList sourceResults) { return new PlanningMapBuildResult(PlanningMapBuildStatus.Failed, reason, sourceResults, PlanningMapCacheHit.None, null); } + internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason stopReason, IReadOnlyList sourceResults) + { + if (stopReason == PlanningOperationStopReason.Cancelled) + return new PlanningMapBuildResult(PlanningMapBuildStatus.Cancelled, "地图创建已取消。", sourceResults, PlanningMapCacheHit.None, null); + if (stopReason == PlanningOperationStopReason.TimedOut) + return new PlanningMapBuildResult(PlanningMapBuildStatus.TimedOut, "地图创建已超时。", sourceResults, PlanningMapCacheHit.None, null); + throw new ArgumentOutOfRangeException(nameof(stopReason)); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs new file mode 100644 index 0000000..1dc7e04 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 规划地图的唯一公开创建入口。 +/// +/// 注意: +/// - 应在规划服务生命周期内长期复用同一实例,才能命中输入和占据两级缓存。 +/// - 本类只消费请求中的纯障碍物快照,不读取传感器、定位、UI 或系统时钟。 +/// +public sealed class PlanningMapFactory +{ + private readonly EnvironmentMapBuilder _builder = new EnvironmentMapBuilder(); + private readonly PlanningMapCache _cache = new PlanningMapCache(); + private readonly object _createGate = new object(); + private long _nextSnapshotId; + + /// + /// 创建规划地图快照。 + /// + /// 参数: + /// - request:完整建图请求;包含世界范围、栅格分辨率、障碍物来源和空图策略,不能为空。 + /// + /// 返回: + /// - PlanningMapBuildResult:成功时包含不可变 PlanningGridMap、每个来源的投影结果和缓存命中类型; + /// 失败时包含失败原因,不提供地图。 + /// + /// 注意: + /// - 完全相同的输入返回同一个快照对象;版本变化但占据相同则复用底层栅格并生成新快照编号。 + /// + public PlanningMapBuildResult Create(PlanningMapRequest request) + { + return Create(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); + } + + /// 使用共享预算创建地图;取消或超时时不发布快照或缓存条目。 + internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget) + { + if (budget == null) throw new ArgumentNullException(nameof(budget)); + PlanningOperationStopReason stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + + bool enteredCreateGate = false; + try + { + while (!Monitor.TryEnter(_createGate, 16)) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + } + enteredCreateGate = true; + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + if (request == null || request.Bounds == null) return PlanningMapBuildResult.Failure("Planning map request and bounds are required.", null); + if (request.ObstacleSources == null) return PlanningMapBuildResult.Failure("Obstacle source collection is required.", null); + string requestKey = BuildRequestKey(request); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); + if (_cache.TryGetInput(requestKey, out PlanningGridMap exactMap, out IReadOnlyList cachedResults)) + { + stopReason = budget.GetStopReason(); + return stopReason == PlanningOperationStopReason.None + ? PlanningMapBuildResult.Success(exactMap, cachedResults, PlanningMapCacheHit.Input) + : PlanningMapBuildResult.Stopped(stopReason, cachedResults); + } + + EnvironmentMapBuildResult environment = _builder.Build( + new MapBuildRequest { Bounds = request.Bounds, ResolutionMm = request.ResolutionMm, ObstacleSources = request.ObstacleSources }, budget); + if (!environment.Succeeded) + return environment.StopReason == PlanningOperationStopReason.None + ? PlanningMapBuildResult.Failure(environment.FailureReason, environment.SourceResults) + : PlanningMapBuildResult.Stopped(environment.StopReason, environment.SourceResults); + + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + string descriptor = BuildInputDescriptor(request, environment.SourceResults); + string inputFingerprint = Sha256(descriptor); + int applied = environment.SourceResults.Count(x => x.Status == ObstacleSourceStatus.Applied); + bool ready = applied > 0 || request.AllowExplicitEmptyMap; + string blockReason = ready ? string.Empty : "No source supplied obstacle geometry; set AllowExplicitEmptyMap only when an intentionally empty map is safe."; + if (!PlanningMapAdapter.TryCreate(environment.Map, budget, out PlanningGridMap candidate, out stopReason)) + return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + if (!TryComputeOccupancyHash(candidate, budget, out string occupancyHash, out stopReason)) + return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + + PlanningGridMap map; + PlanningMapCacheHit cacheHit; + if (_cache.TryGetOccupancy(occupancyHash, candidate, out PlanningGridMap canonical)) + { + map = canonical.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); + cacheHit = PlanningMapCacheHit.Occupancy; + } + else + { + map = candidate.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + _cache.AddOccupancy(occupancyHash, map); + cacheHit = PlanningMapCacheHit.None; + } + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); + _cache.AddInput(requestKey, map, environment.SourceResults); + return PlanningMapBuildResult.Success(map, environment.SourceResults, cacheHit); + } + finally + { + if (enteredCreateGate) Monitor.Exit(_createGate); + } + } + + private long NextSnapshotId() { return Interlocked.Increment(ref _nextSnapshotId); } + + private static string BuildInputDescriptor(PlanningMapRequest request, IReadOnlyList results) + { + var builder = new StringBuilder(); + AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); + var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); + for (int i = 0; i < sources.Length; i++) + { + IMapObstacleSource source = sources[i]; + builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); + if (i < results.Count) AppendProjection(builder, results[i]); + } + return builder.ToString(); + } + private static string BuildRequestKey(PlanningMapRequest request) + { + var builder = new StringBuilder(); + AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); + foreach (IMapObstacleSource source in request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal)) + builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); + return builder.ToString(); + } + private static void AppendProjection(StringBuilder builder, ObstacleProjectionResult result) + { + builder.Append('|').Append((int)result.Status); + for (int index = 0; index < result.Obstacles.Count; index++) + { + var circle = result.Obstacles[index] as CircleObstacle; + if (circle != null) { builder.Append("|C"); AppendFloat(builder, circle.CenterX); AppendFloat(builder, circle.CenterY); AppendFloat(builder, circle.RadiusMm); continue; } + var rectangle = result.Obstacles[index] as AxisAlignedRectangleObstacle; + if (rectangle != null) { builder.Append("|R"); AppendFloat(builder, rectangle.XMin); AppendFloat(builder, rectangle.XMax); AppendFloat(builder, rectangle.YMin); AppendFloat(builder, rectangle.YMax); } + } + } + private static void AppendFloat(StringBuilder builder, float value) { builder.Append(BitConverter.ToInt32(BitConverter.GetBytes(value), 0).ToString("X8")); } + private static bool TryComputeOccupancyHash(PlanningGridMap map, PlanningOperationBudget budget, out string occupancyHash, + out PlanningOperationStopReason stopReason) + { + byte[] occupied = map.CopyOccupied(); + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + using (var sha = SHA256.Create()) + { + const int blockLength = 4096; + int offset = 0; + while (occupied.Length - offset > blockLength) + { + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + sha.TransformBlock(occupied, offset, blockLength, occupied, offset); + offset += blockLength; + } + stopReason = budget.GetStopReason(); + if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } + byte[] hash = sha.TransformFinalBlock(occupied, offset, occupied.Length - offset); + occupancyHash = BitConverter.ToString(hash).Replace("-", string.Empty); + stopReason = PlanningOperationStopReason.None; + return true; + } + } + private static string Sha256(string text) + { + using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs new file mode 100644 index 0000000..26a6822 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 规划地图的完整建图输入。 +/// +/// 注意: +/// - 此对象只描述地图内容,不包含车辆、起终点、PNG 或终端日志等调试参数。 +/// - 障碍物来源快照变化时,调用方必须更新对应的 SourceVersion。 +/// +public sealed class PlanningMapRequest +{ + /// + /// 地图世界边界。 + /// 单位:mm;边界采用左闭右开范围,由 MapBoundsMm 的 XMin、XMax、YMin、YMax 定义。 + /// + public MapBoundsMm Bounds { get; set; } + /// + /// 栅格边长。 + /// 单位:mm;当前 MapBoundsMm 允许的范围为 20 至 200 mm。 + /// + public float ResolutionMm { get; set; } + /// + /// 参与建图的统一障碍物来源。 + /// 参数:列表中的每个来源必须具有唯一 ID 和非负版本号;可为空列表。 + /// + public IReadOnlyList ObstacleSources { get; set; } = Array.Empty(); + /// + /// 是否明确允许没有任何障碍物几何的地图参与规划。 + /// 返回语义:false 时,隐式空图会被标记为不可规划;true 时空图可用于规划。 + /// + public bool AllowExplicitEmptyMap { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/README.md b/ClumsyPilot/ParkrobTrajplanner/Map/README.md new file mode 100644 index 0000000..1d1861d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/README.md @@ -0,0 +1,367 @@ +# Map 模块说明 + +`Map` 为粗路径规划提供只读、可复用的规划地图快照。它只负责把外部障碍物投影并栅格化,生成占据图和保守障碍距离场;不读取传感器、定位、UI 或系统时钟,也不写入 AMR 自身占据和安全外扩。 + +规划器应长期持有一个 `PlanningMapFactory`,通过一次 `Create` 调用取得 `PlanningGridMap`。 + +## 文件结构 + +```text +Map/ +├── README.md # 本模块说明:结构、数据流、单位和调用方式 +├── PlanningMapRequest.cs # 公开建图输入:边界、分辨率、障碍来源、空图策略 +├── PlanningMapBuildResult.cs # 公开建图输出:状态、地图、来源结果、失败原因、缓存命中类型 +├── PlanningMapFactory.cs # 唯一公开建图门面;负责两级缓存和快照编号 +├── Core/ +│ ├── MapBoundsMm.cs # 世界地图范围及行列尺寸计算 +│ ├── MapBuildRequest.cs # EnvironmentMapBuilder 的内部建图输入 +│ ├── EnvironmentMapBuildResult.cs # 环境栅格构建结果 +│ ├── EnvironmentGridMap.cs # 构建期可写的环境占据栅格 +│ └── EnvironmentMapBuilder.cs # 汇总障碍来源并事务性创建环境图 +├── Obstacles/ +│ ├── IMapObstacle.cs # 世界坐标障碍物几何契约 +│ ├── CircleObstacle.cs # 圆形障碍物几何 +│ ├── AxisAlignedRectangleObstacle.cs # 与坐标轴平行的矩形障碍物几何 +│ └── MapObstacleRasterizer.cs # 唯一允许写入环境栅格的障碍物栅格化器 +├── Sources/ +│ ├── IMapObstacleSource.cs # 统一障碍来源接口 +│ ├── ObstacleSourceStatus.cs # 来源投影状态:已应用、空、不可用、无效 +│ ├── ObstacleProjectionResult.cs # 单个来源的世界几何和诊断结果 +│ ├── ManualObstacleSource.cs # 手工输入的圆形/矩形障碍来源 +│ ├── TwoLegProjectionInput.cs # 检测时刻的 TwoLeg 纯数据快照 +│ ├── TwoLegObstacleProjector.cs # 将 TwoLeg 局部坐标投影为世界坐标圆障碍物 +│ └── TwoLegObstacleSource.cs # 将 TwoLeg 快照包装为统一障碍来源 +├── Planning/ +│ ├── PlanningGridMap.cs # 不可变规划快照;规划查询使用米 +│ ├── PlanningMapAdapter.cs # 环境图到规划快照与距离场的适配器 +│ ├── EuclideanDistanceTransform.cs # 二值栅格的精确平方欧氏距离变换 +│ ├── ObstacleDistanceField.cs # 对规划器暴露的保守障碍净距 +│ └── PlanningMapCache.cs # 容量为 4 的输入/占据两级 LRU 缓存 +└── Test/ + ├── MovementTest.MapTest.cs # Clumsy 手工建图测试入口与终端调试开关 + └── Visualization/ + ├── PlanningMapImageExportRequest.cs # PNG 导出输入:规划快照和输出目录 + ├── PlanningMapImageExportResult.cs # PNG 导出状态、路径、尺寸和诊断 + ├── PlanningMapImageExporter.cs # 可选 PNG 导出门面和输出保护 + ├── PlanningMapImageRenderer.cs # 只读快照到 RGBA 像素的渲染器 + └── ValidatedPngWriter.cs # 写入并校验 PNG 结构和 CRC +``` + +## 建图数据流 + +```text +PlanningMapRequest + │ + ▼ +IMapObstacleSource.ProjectToWorld() + │ 输出世界坐标的圆形或矩形几何 + ▼ +EnvironmentMapBuilder + MapObstacleRasterizer + │ 写入构建期 EnvironmentGridMap + ▼ +PlanningMapAdapter + ObstacleDistanceField + │ 生成占据数组和保守距离数组 + ▼ +PlanningGridMap +``` + +具体规则: + +1. 调用者准备 `PlanningMapRequest` 和一个或多个 `IMapObstacleSource`。 +2. 每个来源通过 `ProjectToWorld()` 输出世界坐标几何;Map 核心不主动读取 TwoLeg、定位或其他传感器。 +3. `EnvironmentMapBuilder` 按来源 ID 排序,必需来源失败则整个建图失败;可选来源失败只保留诊断状态。 +4. `MapObstacleRasterizer` 是唯一写入 `EnvironmentGridMap` 占据格的组件。 +5. `PlanningMapAdapter` 生成不可变 `PlanningGridMap`,并附带保守障碍距离场。 +6. `PlanningMapFactory` 返回最终快照及来源投影结果;粗路径规划只应消费这个快照。 + +## 构建状态与停止 + +`PlanningMapBuildResult.Status` 的类型为 `PlanningMapBuildStatus`: + +- `Success`:成功发布不可变 `PlanningGridMap`; +- `Failed`:输入、来源或常规建图失败,读取 `FailureReason`; +- `Cancelled`:调用方取消了带预算的建图,`Map` 为 `null`,不会写入缓存; +- `TimedOut`:建图耗尽调用方的总超时预算,`Map` 为 `null`,不会写入缓存。 + +公开的 `PlanningMapFactory.Create(request)` 保持兼容且不设置时间限制;粗规划门面使用内部预算入口,使取消和超时能够覆盖地图创建、距离场和后续搜索。 + +## 坐标与单位 + +- 环境地图边界、障碍物几何、TwoLeg 投影输入均使用世界坐标,单位为 **mm**。 +- `MapBoundsMm` 范围采用左闭右开:`[XMin, XMax) × [YMin, YMax)`;最大边界不属于地图。 +- `EnvironmentGridMap` 查询使用 mm;`PlanningGridMap` 的世界查询使用 **m**。 +- 行 `row` 对应 Y 方向,列 `col` 对应 X 方向,存储顺序为行主序 `row * Cols + col`。 +- `PlanningGridMap` 的越界位置按占据处理,障碍净距返回 0 m。 +- Map 不做车辆自身占据或安全外扩;车辆外形与安全裕度由后续碰撞检测负责。 + +## 最小调用示例 + +```csharp +var mapFactory = new PlanningMapFactory(); // 长期持有,不要每次规划重新创建 + +IMapObstacle[] obstacles = +{ + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), +}; + +var request = new PlanningMapRequest +{ + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, obstacles), + }, + AllowExplicitEmptyMap = false, +}; + +PlanningMapBuildResult result = mapFactory.Create(request); +if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + throw new InvalidOperationException(result.FailureReason); + +PlanningGridMap map = result.Map; // 交给粗路径规划器 +``` + +真实 TwoLeg 数据应由上层检测模块在检测时刻构造成 `TwoLegProjectionInput`,再交给 `TwoLegObstacleSource`。不要让 Map 模块主动读取检测器或定位器。 + +## 缓存与版本 + +`PlanningMapFactory` 内部有容量为 4 的两级 LRU 缓存: + +- **输入命中(Input)**:边界、分辨率、空图策略、来源 ID、`SourceVersion` 和必需性均相同,直接返回同一个 `PlanningGridMap` 对象。 +- **占据命中(Occupancy)**:来源版本变化,但最终占据栅格相同,复用不可变的占据/距离数组,并颁发新的 `SnapshotId`。 +- **未命中(None)**:栅格内容变化,重新生成规划快照。 + +因此,障碍来源的快照内容发生变化时,调用方必须增加其 `SourceVersion`。未递增版本会错误复用旧地图;仅修改日志、PNG 开关、起终点或车辆参数不应改变地图版本。 + +## 测试与调试 + +- `Test/MovementTest.MapTest.cs` 是手工 Clumsy 测试入口。文件顶部可设置地图范围、分辨率、TwoLeg 测试快照、`EnableTerminalDebugLog` 和 `SavePng`。 +- `PlanningMapImageExporter` 仅在 `SavePng` 开启时输出 PNG;它只读取 `PlanningGridMap`,不参与建图、缓存键或规划结果。 +- 自动检查脚本位于 `ClumsyPilot/tests`:工具、工厂、适配器、PNG 和 MapTest 配置分别有独立验证脚本。 +- 旧版 `Occupancygird_Map/Map_test/TrapMapImageExporter.cs` 与 `MovementTest.Trapmaptest.cs` 仍保留作历史对照;它们不是新粗路径规划的运行时地图入口。 + +## 详细使用指南 + +本节说明调用方如何从“障碍物数据”逐步得到可交给粗路径规划器的 `PlanningGridMap`。新地图的唯一创建入口是: + +```csharp +PlanningMapBuildResult result = mapFactory.Create(request); +``` + +其中 `mapFactory` 是长期持有的 `PlanningMapFactory`,`request` 是本次建图输入,`result.Map` 是成功时的只读规划地图。 + +### 第 1 步:长期创建地图工厂 + +地图工厂内部维护容量为 4 的缓存,因此不要在每次规划前重新创建它。应把它作为规划服务或 MovementTest 的字段长期保存: + +```csharp +using MultiWheelC.TrajectoryPlanning.Mapping; + +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); +``` + +### 第 2 步:准备手工障碍物 + +圆形和轴对齐矩形都实现 `IMapObstacle`。障碍物的坐标是**世界坐标**,单位都是 **mm**。 + +```csharp +IMapObstacle[] manualObstacles = +{ + // 参数依次为:X最小值、X最大值、Y最小值、Y最大值,单位均为 mm。 + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + + // 参数依次为:圆心X、圆心Y、半径,单位均为 mm。 + new CircleObstacle(3600f, 1200f, 180f), +}; +``` + +目前 Map 只负责“环境障碍物”。不要在这里添加 AMR 自身外形,也不要在圆半径或矩形尺寸中叠加安全裕度;车辆外形与安全距离由粗路径的碰撞检查处理。 + +### 第 3 步:包装为统一障碍来源 + +所有障碍物必须通过 `IMapObstacleSource` 进入地图。手工障碍使用 `ManualObstacleSource`: + +```csharp +var manualSource = new ManualObstacleSource( + sourceId: "manual", + sourceVersion: 1L, + isRequired: true, + obstacles: manualObstacles); +``` + +参数意义如下: + +- `sourceId`:来源的唯一名称;同一次请求内不能重复。 +- `sourceVersion`:来源快照版本。障碍物位置、数量、半径或尺寸变化后,必须递增。 +- `isRequired`:`true` 表示来源无效时整次建图失败;`false` 表示仅记录该来源状态并继续建图。 +- `obstacles`:当前时刻的不可变障碍物集合。 + +例如障碍物内容变化后,应创建带新版本号的来源: + +```csharp +var changedManualSource = new ManualObstacleSource( + "manual", + 2L, // 1L 变为 2L,通知工厂地图输入已改变 + true, + changedObstacles); +``` + +### 第 4 步:可选地加入 TwoLeg 障碍物 + +Map 不主动调用 TwoLeg 检测器。上层检测模块应在检测时刻取得 AMR 世界位姿和两腿局部坐标,构造成 `TwoLegProjectionInput`: + +```csharp +var twoLegInput = new TwoLegProjectionInput( + hasDetection: true, + + // 检测时 AMR 的世界位姿:位置单位 mm,航向单位 rad。 + detectionWorldX: 1000f, + detectionWorldY: 2000f, + detectionHeadingRadians: 0d, + + // 两条腿相对于检测时 AMR 位姿的局部坐标,单位 mm。 + firstLocalX: 300f, + firstLocalY: 150f, + secondLocalX: 300f, + secondLocalY: -150f, + + // 每条腿最终投影为圆形障碍物的半径,单位 mm。 + radiusMm: 80f, + diagnostic: "TwoLeg 检测快照"); + +var twoLegSource = new TwoLegObstacleSource( + sourceId: "two-leg", + sourceVersion: 5L, + isRequired: false, + input: twoLegInput); +``` + +没有检测结果时仍可传入空快照: + +```csharp +var noTwoLegInput = new TwoLegProjectionInput( + false, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, + "当前没有 TwoLeg 检测结果"); +``` + +`hasDetection` 为 `false` 时,该来源会返回“空”结果,不会将零坐标当作障碍物。若 TwoLeg 只是可选信息,建议 `isRequired` 设置为 `false`。 + +### 第 5 步:创建建图请求 + +边界与分辨率仍使用 **mm**。范围采用左闭右开,例如 `XMax = 6000f` 时,`x = 6000f` 不属于地图。 + +```csharp +var request = new PlanningMapRequest +{ + // 世界范围:[0, 6000) × [0, 4000),单位 mm。 + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + + // 每格 50 mm。 + ResolutionMm = 50f, + + ObstacleSources = new IMapObstacleSource[] + { + manualSource, + twoLegSource, + }, + + // false:没有任何有效障碍物时,地图不能直接交给规划器。 + // true:调用方明确确认空地图安全时,才允许空图参与规划。 + AllowExplicitEmptyMap = false, +}; +``` + +### 第 6 步:创建地图并处理失败 + +```csharp +PlanningMapBuildResult result = _mapFactory.Create(request); + +if (!result.Succeeded) +{ + Console.WriteLine("建图失败:" + result.FailureReason); + return; +} + +if (result.Map == null || !result.Map.PlanningReady) +{ + Console.WriteLine("地图不能用于规划:" + result.Map?.PlanningBlockReason); + return; +} + +PlanningGridMap planningMap = result.Map; +``` + +此处必须同时检查: + +- `Succeeded`:请求、来源和栅格构建过程是否成功; +- `Map != null`:是否产出了规划快照; +- `PlanningReady`:是否允许把该快照交给规划器。隐式空图会在这一项被拦截。 + +需要查看每个来源是否成功投影时,读取 `result.SourceResults`;需要观察缓存效果时,读取 `result.CacheHit`。 + +```csharp +Console.WriteLine( + "快照编号=" + planningMap.SnapshotId + + ",缓存=" + result.CacheHit + + ",栅格=" + planningMap.Cols + "×" + planningMap.Rows); +``` + +### 第 7 步:交给粗路径规划器查询 + +`PlanningGridMap` 是不可变对象,可以安全地作为一次规划任务的输入。注意:它的世界查询坐标单位已经变成 **m**,而不是 mm。 + +```csharp +// 查询 (2.5 m, 1.0 m) 是否位于占据格;越界也会返回 true。 +bool occupied = planningMap.IsOccupiedWorld(2.5d, 1.0d); + +// 查询该位置到最近障碍物的保守净距,单位 m;越界返回 0 m。 +double clearanceMeters = + planningMap.GetConservativeObstacleDistanceMeters(2.5d, 1.0d); +``` + +粗路径规划器应只使用 `PlanningGridMap` 的占据与距离查询,不应直接修改或重建其中的栅格。 + +### 完整实例 + +```csharp +private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + +private PlanningGridMap CreateMapForCoarsePlanning() +{ + IMapObstacle[] obstacles = + { + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), + }; + + var manualSource = new ManualObstacleSource("manual", 1L, true, obstacles); + + var request = new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] { manualSource }, + AllowExplicitEmptyMap = false, + }; + + PlanningMapBuildResult result = _mapFactory.Create(request); + if (!result.Succeeded) + throw new InvalidOperationException("建图失败:" + result.FailureReason); + if (result.Map == null || !result.Map.PlanningReady) + throw new InvalidOperationException("地图不可规划:" + result.Map?.PlanningBlockReason); + + return result.Map; +} +``` + +### 常见错误 + +| 情况 | 原因 | 处理方式 | +| --- | --- | --- | +| 改了障碍物但地图仍复用旧快照 | 未递增 `SourceVersion` | 障碍内容每次变化后增加该来源版本号 | +| 规划查询位置总是越界 | 将 mm 坐标传给了 `PlanningGridMap` | 规划查询前将 mm 除以 1000 转为 m | +| 地图创建成功但不能规划 | 未明确允许空图且没有有效障碍物 | 补充有效来源,或确认安全后设置 `AllowExplicitEmptyMap = true` | +| TwoLeg 出现在错误位置 | 未使用检测时刻位姿,或混用了 mm 与 m | 用检测时刻的世界位姿和局部 mm 坐标创建快照 | +| 缓存总是未命中 | 每次都新建 `PlanningMapFactory`,或版本号无意义变化 | 长期复用工厂;仅在来源内容实际变化时递增版本 | diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs new file mode 100644 index 0000000..ca0312f --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs @@ -0,0 +1,22 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// 提供一次性障碍物快照的纯数据来源。 +/// +/// 注意: 不得读取传感器、定位、UI 或时钟;调用前应由上层采集并封装完整快照。 +/// +public interface IMapObstacleSource +{ + /// 来源的稳定唯一标识。不能为空;同一次建图请求中不得重复。 + string SourceId { get; } + /// 来源快照版本号,必须非负。快照几何或有效性变化时必须递增,以使输入缓存失效。 + long SourceVersion { get; } + /// 该来源是否必需。true 时不可用或无效会使整张地图构建失败;false 时只记录状态。 + bool IsRequired { get; } + /// + /// 将已采集的快照投影为世界障碍物几何。 + /// + /// 返回:世界坐标、单位 mm 的障碍物及其状态;不得访问任何实时外部状态。 + /// + ObstacleProjectionResult ProjectToWorld(); +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs new file mode 100644 index 0000000..e4b9334 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 直接提供世界 mm 几何列表的手工障碍物快照来源。 +public sealed class ManualObstacleSource : IMapObstacleSource +{ + private readonly IReadOnlyList _obstacles; + /// + /// 创建手工障碍物来源。 + /// + /// 参数:sourceId 为唯一来源标识;sourceVersion 为非负快照版本;isRequired 指示失败是否阻断建图;obstacles 为世界 mm 几何列表。 + /// 注意:几何内容变化时,调用方必须同步提高 sourceVersion。 + /// + public ManualObstacleSource(string sourceId, long sourceVersion, bool isRequired, IReadOnlyList obstacles) + { + SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _obstacles = obstacles; + } + /// 该快照的唯一来源标识。 + public string SourceId { get; } + /// 该快照的版本号;内容变化时必须递增。 + public long SourceVersion { get; } + /// true 表示无效来源会阻断建图;false 表示只记录来源状态。 + public bool IsRequired { get; } + /// + /// 返回手工障碍物的世界几何。 + /// + /// 返回:列表为空时为 Empty;列表为空引用或含无效几何时为 Invalid;其余情况为 Applied。 + /// + public ObstacleProjectionResult ProjectToWorld() + { + if (_obstacles == null) return ObstacleProjectionResult.Invalid("Manual obstacle collection is null."); + if (_obstacles.Count == 0) return ObstacleProjectionResult.Empty("Manual obstacle collection is empty."); + for (int i = 0; i < _obstacles.Count; i++) + if (_obstacles[i] == null || !_obstacles[i].IsValid) return ObstacleProjectionResult.Invalid("Manual obstacle geometry is invalid."); + return ObstacleProjectionResult.Applied(_obstacles, "Manual obstacle snapshot applied."); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs new file mode 100644 index 0000000..b429f26 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 障碍物来源的一次投影结果,包含状态、世界 mm 几何和可选诊断信息。 +public sealed class ObstacleProjectionResult +{ + private ObstacleProjectionResult(ObstacleSourceStatus status, IReadOnlyList obstacles, string diagnostic) + { + Status = status; Obstacles = obstacles ?? Array.Empty(); Diagnostic = diagnostic ?? string.Empty; + } + /// 投影状态。只有 的几何会被栅格化。 + public ObstacleSourceStatus Status { get; } + /// 世界 mm 坐标的障碍物列表。非 Applied 状态时为空列表,不为 null。 + public IReadOnlyList Obstacles { get; } + /// 用于日志和诊断的说明文本;未提供时为空字符串。 + public string Diagnostic { get; } + /// 创建成功投影结果。参数 obstacles 为有效世界 mm 几何;diagnostic 为可选诊断文本。返回的状态为 Applied。 + public static ObstacleProjectionResult Applied(IReadOnlyList obstacles, string diagnostic = null) + { + return new ObstacleProjectionResult(ObstacleSourceStatus.Applied, obstacles, diagnostic); + } + /// 创建空投影结果。参数 diagnostic 为可选原因;返回状态为 Empty,障碍物列表为空。 + public static ObstacleProjectionResult Empty(string diagnostic = null) { return new ObstacleProjectionResult(ObstacleSourceStatus.Empty, null, diagnostic); } + /// 创建不可用投影结果。参数 diagnostic 应说明快照缺失原因;返回状态为 Unavailable。 + public static ObstacleProjectionResult Unavailable(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Unavailable, null, diagnostic); } + /// 创建无效投影结果。参数 diagnostic 应说明数据或几何错误;返回状态为 Invalid。 + public static ObstacleProjectionResult Invalid(string diagnostic) { return new ObstacleProjectionResult(ObstacleSourceStatus.Invalid, null, diagnostic); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs new file mode 100644 index 0000000..f71161a --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs @@ -0,0 +1,14 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 一个障碍物来源投影后的状态,供建图结果和调试日志判断。 +public enum ObstacleSourceStatus +{ + /// 成功得到一个或多个有效的世界 mm 障碍物,并会被栅格化。 + Applied, + /// 来源有效但本次没有障碍物;不会写入任何栅格。 + Empty, + /// 来源快照不可取得;若来源必需则整图构建失败。 + Unavailable, + /// 来源数据或几何无效;若来源必需则整图构建失败。 + Invalid, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs new file mode 100644 index 0000000..50afab3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将检测局部坐标中的两条腿转换为世界 mm 坐标圆形障碍物。 +public static class TwoLegObstacleProjector +{ + /// + /// 投影一份 TwoLeg 快照。 + /// + /// 参数:input 为检测时刻采集的纯数据快照,可为 null。 + /// 返回:null 为 Unavailable;未检测到目标为 Empty;数值无效为 Invalid;成功时为含两个圆形障碍物的 Applied。 + /// 注意:本方法只读取 input,不访问传感器、定位、UI 或时钟。 + /// + public static ObstacleProjectionResult Project(TwoLegProjectionInput input) + { + if (input == null) return ObstacleProjectionResult.Unavailable("TwoLeg snapshot is unavailable."); + if (!input.HasDetection) return ObstacleProjectionResult.Empty(input.Diagnostic); + if (!input.IsValid) return ObstacleProjectionResult.Invalid("TwoLeg snapshot contains invalid values."); + CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, + input.FirstLocalX, input.FirstLocalY, out double firstX, out double firstY); + CoordinateTransform.LocalToWorld(input.DetectionWorldX, input.DetectionWorldY, input.DetectionHeadingRadians, + input.SecondLocalX, input.SecondLocalY, out double secondX, out double secondY); + var obstacles = new List + { + new CircleObstacle((float)firstX, (float)firstY, input.RadiusMm), + new CircleObstacle((float)secondX, (float)secondY, input.RadiusMm), + }; + return ObstacleProjectionResult.Applied(obstacles, input.Diagnostic); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs new file mode 100644 index 0000000..206fd4b --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs @@ -0,0 +1,24 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 将一个 快照包装为统一障碍物来源。 +public sealed class TwoLegObstacleSource : IMapObstacleSource +{ + private readonly TwoLegProjectionInput _input; + /// + /// 创建 TwoLeg 障碍物来源。 + /// + /// 参数:sourceId 为唯一标识;sourceVersion 为快照版本,输入内容改变时必须递增;isRequired 指示失败是否阻断建图;input 为检测时刻快照,可为空。 + /// + public TwoLegObstacleSource(string sourceId, long sourceVersion, bool isRequired, TwoLegProjectionInput input) + { + SourceId = sourceId; SourceVersion = sourceVersion; IsRequired = isRequired; _input = input; + } + /// 该快照来源的唯一标识。 + public string SourceId { get; } + /// 快照版本号;输入内容变化时必须递增。 + public long SourceVersion { get; } + /// true 时投影不可用或无效会阻断建图。 + public bool IsRequired { get; } + /// 投影保存的 TwoLeg 快照。返回世界 mm 圆形障碍物,且不访问实时检测、定位、UI 或时钟。 + public ObstacleProjectionResult ProjectToWorld() { return TwoLegObstacleProjector.Project(_input); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs new file mode 100644 index 0000000..bb1ed94 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs @@ -0,0 +1,51 @@ +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// +/// TwoLeg 检测时刻的纯输入快照。 +/// +/// 单位:位置和两腿局部坐标均为 mm,航向为弧度。该对象不读取检测器或定位模块,确保投影可重复。 +/// +public sealed class TwoLegProjectionInput +{ + /// + /// 创建 TwoLeg 投影快照。 + /// + /// 参数:hasDetection 表示当前是否检测到目标;detectionWorldX/Y 为检测坐标系原点的世界 mm 坐标;detectionHeadingRadians 为其相对世界坐标系的航向;firstLocalX/Y、secondLocalX/Y 为两腿在检测局部坐标系中的 mm 坐标;radiusMm 为每条腿投影圆半径;diagnostic 为可选诊断文本。 + /// 注意:hasDetection 为 false 时其余几何参数不会参与有效性校验。 + /// + public TwoLegProjectionInput(bool hasDetection, float detectionWorldX, float detectionWorldY, + double detectionHeadingRadians, float firstLocalX, float firstLocalY, + float secondLocalX, float secondLocalY, float radiusMm, string diagnostic = null) + { + HasDetection = hasDetection; DetectionWorldX = detectionWorldX; DetectionWorldY = detectionWorldY; + DetectionHeadingRadians = detectionHeadingRadians; FirstLocalX = firstLocalX; FirstLocalY = firstLocalY; + SecondLocalX = secondLocalX; SecondLocalY = secondLocalY; RadiusMm = radiusMm; Diagnostic = diagnostic ?? string.Empty; + } + /// 检测是否存在。false 时投影结果为 Empty。 + public bool HasDetection { get; } + /// 检测局部坐标系原点的世界 X 坐标,单位 mm。 + public float DetectionWorldX { get; } + /// 检测局部坐标系原点的世界 Y 坐标,单位 mm。 + public float DetectionWorldY { get; } + /// 检测局部坐标系相对世界坐标系的航向,单位弧度。 + public double DetectionHeadingRadians { get; } + /// 第一条腿在检测局部坐标系的 X 坐标,单位 mm。 + public float FirstLocalX { get; } + /// 第一条腿在检测局部坐标系的 Y 坐标,单位 mm。 + public float FirstLocalY { get; } + /// 第二条腿在检测局部坐标系的 X 坐标,单位 mm。 + public float SecondLocalX { get; } + /// 第二条腿在检测局部坐标系的 Y 坐标,单位 mm。 + public float SecondLocalY { get; } + /// 投影为圆形障碍物时的半径,单位 mm。 + public float RadiusMm { get; } + /// 上游检测阶段提供的可选诊断文本;未提供时为空字符串。 + public string Diagnostic { get; } + /// 未检测到目标时为 true;检测到目标时,所有数值有限且半径非负才为 true。 + public bool IsValid + { + get { return !HasDetection || (NumericGuard.IsFinite(DetectionWorldX) && NumericGuard.IsFinite(DetectionWorldY) && NumericGuard.IsFinite(DetectionHeadingRadians) && NumericGuard.IsFinite(FirstLocalX) && NumericGuard.IsFinite(FirstLocalY) && NumericGuard.IsFinite(SecondLocalX) && NumericGuard.IsFinite(SecondLocalY) && NumericGuard.IsFinite(RadiusMm) && RadiusMm >= 0f); } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs new file mode 100644 index 0000000..5a8eb2e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using ClumsyCore; +using ClumsyCore.DTools; +using ClumsyCore.Interfaces; +using ClumsyCore.Pilot; +using CommonUsage.Mathematics; +using FundamentalLib; +using MDCSToolBox.Clumsy.Movements; +using MDCSToolBox.Clumsy.Pilot; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC; + +/// +/// 用于手工调试的 Clumsy 地图测试入口,只通过公开的 创建快照。 +/// +/// 注意:终端日志和 PNG 开关只影响调试输出,不参与建图输入、地图内容或缓存键。 +/// +[MovementTest(name = "规划地图快照测试V1")] +public sealed class PlanningMapTest : MovementTest +{ + private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); + private const bool EnableTerminalDebugLog = true; + private const bool SavePng = true; + private const float MapXMinMm = 0f; + private const float MapXMaxMm = 6000f; + private const float MapYMinMm = 0f; + private const float MapYMaxMm = 4000f; + private const float GridResolutionMm = 50f; + private const bool AllowExplicitEmptyMap = false; + private const bool EnableTwoLegSnapshot = false; + + /// + /// 执行一次配置好的地图创建与调试输出。 + /// + /// 返回:无。测试使用本文件顶部的地图范围、分辨率、障碍物来源、日志和 PNG 开关;不会接入实时 TwoLeg 检测。 + /// + public override void Test() + { + IMapObstacleSource[] sources = CreateObstacleSources(); + Log("========== 规划地图创建开始 =========="); + Log("参数:地图范围=(" + MapXMinMm + "," + MapXMaxMm + ")x(" + MapYMinMm + "," + MapYMaxMm + + ")mm,分辨率=" + GridResolutionMm + "mm,允许空图=" + AllowExplicitEmptyMap + ",导出图片=" + SavePng); + Log("障碍物来源:" + FormatSources(sources)); + var result = _mapFactory.Create(new PlanningMapRequest + { + Bounds = new MapBoundsMm(MapXMinMm, MapXMaxMm, MapYMinMm, MapYMaxMm), + ResolutionMm = GridResolutionMm, + ObstacleSources = sources, + AllowExplicitEmptyMap = AllowExplicitEmptyMap, + }); + Log("投影结果:" + FormatProjectionResults(sources, result.SourceResults)); + if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) + { + Log("规划地图创建失败:" + result.FailureReason); + Log("========== 规划地图创建结束 =========="); + return; + } + Log("地图快照:编号=" + result.Map.SnapshotId + ",缓存=" + FormatCacheHit(result.CacheHit) + ",栅格=" + result.Map.Cols + "x" + result.Map.Rows + + ",可规划=" + result.Map.PlanningReady + ",指纹=" + result.Map.InputFingerprint); + PlanningMapImageExportResult image = PlanningMapImageExporter.ExportIfEnabled(SavePng, + new PlanningMapImageExportRequest { Map = result.Map, OutputRootDirectory = Environment.CurrentDirectory }); + Log("图片导出:" + FormatImageResult(image)); + Log("========== 规划地图创建完成 =========="); + } + + private static IMapObstacleSource[] CreateObstacleSources() + { + IMapObstacle[] manualObstacles = + { + new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), + new CircleObstacle(3600f, 1200f, 180f), + }; + return new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, manualObstacles), + new TwoLegObstacleSource("two-leg", EnableTwoLegSnapshot ? 1L : 0L, false, + new TwoLegProjectionInput(EnableTwoLegSnapshot, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, "No current TwoLeg snapshot.")), + }; + } + + private static string FormatSources(IReadOnlyList sources) + { + return string.Join(";", sources.Select(source => source.SourceId + "(版本=" + source.SourceVersion + ",必需=" + source.IsRequired + ")")); + } + + private static string FormatProjectionResults(IReadOnlyList sources, IReadOnlyList results) + { + IMapObstacleSource[] sortedSources = sources.OrderBy(source => source.SourceId, StringComparer.Ordinal).ToArray(); + return string.Join(";", results.Select((result, index) => sortedSources[index].SourceId + "=" + FormatSourceStatus(result.Status) + "(" + result.Obstacles.Count + " 个障碍物)")); + } + + private static string FormatSourceStatus(ObstacleSourceStatus status) + { + switch (status) + { + case ObstacleSourceStatus.Applied: return "已应用"; + case ObstacleSourceStatus.Empty: return "空"; + case ObstacleSourceStatus.Unavailable: return "不可用"; + case ObstacleSourceStatus.Invalid: return "无效"; + default: return "未知"; + } + } + + private static string FormatCacheHit(PlanningMapCacheHit cacheHit) + { + switch (cacheHit) + { + case PlanningMapCacheHit.Input: return "输入命中"; + case PlanningMapCacheHit.Occupancy: return "占据图命中"; + default: return "未命中"; + } + } + + private static string FormatImageResult(PlanningMapImageExportResult image) + { + if (image.Skipped) return "未启用"; + if (image.Saved) return "已保存:" + image.FilePath; + return "失败:" + image.Message; + } + + private static void Log(string message) + { + if (EnableTerminalDebugLog) Console.WriteLine("[PlanningMapTest] " + message); + } + + /// 停止测试。当前测试不启动持续任务,因此无需额外清理资源。 + public override void TestStop() { } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs new file mode 100644 index 0000000..3abe48b --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs @@ -0,0 +1,10 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 可选 PNG 调试图导出请求,只读取不可变的规划地图快照。 +public sealed class PlanningMapImageExportRequest +{ + /// 待渲染的规划地图快照。不能为空;PNG 导出不会改动该地图或其缓存。 + public PlanningGridMap Map { get; set; } + /// PNG 输出根目录。导出器会在其中创建 PlanningMapExports 子目录;不能为空。 + public string OutputRootDirectory { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs new file mode 100644 index 0000000..7d09fce --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs @@ -0,0 +1,20 @@ +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 一次可选 PNG 导出的处理结果,包含状态、输出路径、像素尺寸和诊断信息。 +public sealed class PlanningMapImageExportResult +{ + /// 是否已经成功写入并完成 PNG 文件。 + public bool Saved { get; set; } + /// 是否因导出开关关闭而跳过。true 时不创建文件,也不影响地图构建或缓存。 + public bool Skipped { get; set; } + /// 成功保存时的 PNG 完整路径;未保存时通常为 null。 + public string FilePath { get; set; } + /// 保存、跳过或失败的诊断信息。 + public string Message { get; set; } + /// 成功 PNG 的文件字节数;未保存时为零。 + public long FileSizeBytes { get; set; } + /// 输出图像宽度,单位为像素。 + public int PixelWidth { get; set; } + /// 输出图像高度,单位为像素。 + public int PixelHeight { get; set; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs new file mode 100644 index 0000000..be32cd4 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 可选 PNG 导出器;它永远不参与地图构建、障碍物投影或缓存指纹计算。 +public static class PlanningMapImageExporter +{ + /// 每个规划栅格在输出图中占用的边长,单位为像素。 + public const int PixelsPerCell = 4; + /// 输出 PNG 单边允许的最大像素数,超过时拒绝导出。 + public const int MaximumImageEdgePixels = 4000; + /// 输出 PNG 允许的最大文件大小,单位为字节(50 MiB)。 + public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; + /// 写入 PNG 的物理分辨率元数据,单位 DPI。 + public const float OutputDpi = 300f; + private const int MaximumFilenameAttempts = 1024; + + /// + /// 在开关开启时导出规划地图 PNG。 + /// + /// 参数:enabled 为导出开关;request 包含不可变地图和输出根目录。 + /// 返回:开关关闭时返回 Skipped;成功时返回 Saved、路径、字节数与像素尺寸;输入或尺寸不合法时返回失败信息。 + /// 注意:该方法仅用于调试,绝不会影响地图创建和缓存结果。 + /// + public static PlanningMapImageExportResult ExportIfEnabled(bool enabled, PlanningMapImageExportRequest request) + { + if (!enabled) return new PlanningMapImageExportResult { Skipped = true, Message = "Planning map image export is disabled." }; + if (request == null || request.Map == null) return Rejected("Planning map image export requires a PlanningGridMap."); + if (string.IsNullOrWhiteSpace(request.OutputRootDirectory)) return Rejected("Planning map image export requires an output root directory."); + long width = (long)request.Map.Cols * PixelsPerCell, height = (long)request.Map.Rows * PixelsPerCell; + if (width <= 0 || height <= 0 || width > MaximumImageEdgePixels || height > MaximumImageEdgePixels) return Rejected("Planning map image dimensions exceed the permitted edge.", width, height); + string temporary = null; + try + { + string directory = Path.Combine(request.OutputRootDirectory, "PlanningMapExports"); + Directory.CreateDirectory(directory); + using (FileStream stream = CreateTemporaryFile(directory, out string finalPath, out temporary)) + { + byte[] rgba = PlanningMapImageRenderer.Render(request.Map, PixelsPerCell, out int pixelWidth, out int pixelHeight); + ValidatedPngWriter.Write(rgba, pixelWidth, pixelHeight, stream); + } + long length = new FileInfo(temporary).Length; + if (length > MaximumFileSizeBytes) return Rejected("Planning map image PNG exceeds 50 MiB.", width, height); + string completed = temporary.Substring(0, temporary.Length - 4); + File.Move(temporary, completed); temporary = null; + return new PlanningMapImageExportResult { Saved = true, FilePath = completed, FileSizeBytes = length, PixelWidth = (int)width, PixelHeight = (int)height, Message = "Planning map image export saved." }; + } + catch (Exception exception) { return Rejected("Planning map image export failed: " + exception.Message, width, height); } + finally { if (temporary != null && File.Exists(temporary)) File.Delete(temporary); } + } + + private static PlanningMapImageExportResult Rejected(string message, long width = 0, long height = 0) { return new PlanningMapImageExportResult { Message = message, PixelWidth = width > int.MaxValue ? int.MaxValue : (int)width, PixelHeight = height > int.MaxValue ? int.MaxValue : (int)height }; } + private static FileStream CreateTemporaryFile(string directory, out string finalPath, out string temporaryPath) + { + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); + for (int index = 0; index < MaximumFilenameAttempts; index++) + { + string suffix = index == 0 ? string.Empty : "_" + index; + finalPath = Path.Combine(directory, "PlanningMap_" + timestamp + suffix + ".png"); temporaryPath = finalPath + ".tmp"; + if (File.Exists(finalPath)) continue; + try { return new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); } + catch (IOException) { } + } + finalPath = null; temporaryPath = null; throw new IOException("Could not reserve a unique PlanningMap PNG filename."); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs new file mode 100644 index 0000000..fbdbdbf --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs @@ -0,0 +1,37 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 从只读规划地图生成简单的 RGBA 占据图字节数组。 +internal static class PlanningMapImageRenderer +{ + /// + /// 渲染规划地图为行主序 RGBA 像素。 + /// + /// 参数:map 为只读规划快照;pixelsPerCell 为每个栅格的像素边长;width、height 返回图像像素尺寸。 + /// 返回:长度为 width × height × 4 的 RGBA 字节数组;不会修改地图。 + /// + public static byte[] Render(PlanningGridMap map, int pixelsPerCell, out int width, out int height) + { + if (map == null) throw new ArgumentNullException(nameof(map)); + width = checked(map.Cols * pixelsPerCell); + height = checked(map.Rows * pixelsPerCell); + var rgba = new byte[checked(width * height * 4)]; + for (int row = 0; row < map.Rows; row++) + for (int col = 0; col < map.Cols; col++) + { + bool occupied = map.IsOccupied(row, col); + byte red = occupied ? (byte)220 : (byte)245; + byte green = occupied ? (byte)45 : (byte)245; + byte blue = occupied ? (byte)45 : (byte)245; + int displayRow = map.Rows - 1 - row; + for (int py = 0; py < pixelsPerCell; py++) + for (int px = 0; px < pixelsPerCell; px++) + { + int index = ((displayRow * pixelsPerCell + py) * width + col * pixelsPerCell + px) * 4; + rgba[index] = red; rgba[index + 1] = green; rgba[index + 2] = blue; rgba[index + 3] = 255; + } + } + return rgba; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs new file mode 100644 index 0000000..d2a6371 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using StbImageWriteSharp; + +namespace MultiWheelC.TrajectoryPlanning.Mapping; + +/// 编码 RGBA 像素并校验 PNG 文件头、必要块和全部 CRC 的内部写入器。 +internal static class ValidatedPngWriter +{ + private static readonly byte[] Signature = { 137, 80, 78, 71, 13, 10, 26, 10 }; + private static readonly byte[] PhysType = { 112, 72, 89, 115 }; + + /// + /// 将 RGBA 像素写入经过校验的 PNG 流。 + /// + /// 参数:rgba 为行主序、每像素四字节的红绿蓝透明度数组;width、height 为像素尺寸;output 为可写目标流。 + /// 返回:无。输入长度不等于 width × height × 4 或 PNG 校验失败时抛出异常。 + /// + public static void Write(byte[] rgba, int width, int height, Stream output) + { + Write(rgba, width, height, output, 11811u); + } + + /// 将 RGBA 像素写入带指定物理分辨率元数据的经过校验的 PNG 流。 + internal static void Write(byte[] rgba, int width, int height, Stream output, uint pixelsPerMeter) + { + if (rgba == null || output == null || width <= 0 || height <= 0 || pixelsPerMeter == 0u || rgba.Length != checked(width * height * 4)) + throw new ArgumentException("Invalid RGBA PNG input."); + byte[] encoded; + using (var memory = new MemoryStream()) + { + new ImageWriter().WritePng(rgba, width, height, ColorComponents.RedGreenBlueAlpha, memory); + encoded = memory.ToArray(); + } + Validate(encoded); + const int ihdrEndOffset = 8 + 4 + 4 + 13 + 4; + using (var outputMemory = new MemoryStream()) + { + outputMemory.Write(encoded, 0, ihdrEndOffset); + var phys = new byte[9]; + WriteUInt32BigEndian(phys, 0, pixelsPerMeter); + WriteUInt32BigEndian(phys, 4, pixelsPerMeter); + phys[8] = 1; + WriteChunk(outputMemory, PhysType, phys); + outputMemory.Write(encoded, ihdrEndOffset, encoded.Length - ihdrEndOffset); + encoded = outputMemory.ToArray(); + } + Validate(encoded); + output.Write(encoded, 0, encoded.Length); + } + + private static void Validate(byte[] png) + { + if (png == null || png.Length < 45) throw new InvalidDataException("PNG is truncated."); + for (int i = 0; i < Signature.Length; i++) if (png[i] != Signature[i]) throw new InvalidDataException("PNG signature is invalid."); + int offset = Signature.Length, ihdr = 0, iend = 0; + while (offset < png.Length) + { + if (png.Length - offset < 12) throw new InvalidDataException("PNG chunk header is truncated."); + uint length = ReadUInt32BigEndian(png, offset); + long crcOffset = (long)offset + 8L + length; + if (crcOffset + 4L > png.Length) throw new InvalidDataException("PNG chunk is truncated."); + int typeOffset = offset + 4; + uint expected = ReadUInt32BigEndian(png, (int)crcOffset); + uint actual = ComputeCrc32(png, typeOffset, checked((int)length + 4)); + if (expected != actual) throw new InvalidDataException("PNG chunk CRC is invalid."); + bool isIhdr = IsType(png, typeOffset, 73, 72, 68, 82); + bool isIend = IsType(png, typeOffset, 73, 69, 78, 68); + if (offset == Signature.Length && !isIhdr) throw new InvalidDataException("PNG must start with IHDR."); + if (isIhdr) ihdr++; + if (isIend) { iend++; if (crcOffset + 4L != png.Length) throw new InvalidDataException("PNG data follows IEND."); } + offset = checked((int)crcOffset + 4); + } + if (ihdr != 1 || iend != 1) throw new InvalidDataException("PNG must contain exactly one IHDR and IEND."); + } + private static bool IsType(byte[] bytes, int offset, byte a, byte b, byte c, byte d) { return bytes[offset] == a && bytes[offset + 1] == b && bytes[offset + 2] == c && bytes[offset + 3] == d; } + private static uint ReadUInt32BigEndian(byte[] bytes, int offset) { return ((uint)bytes[offset] << 24) | ((uint)bytes[offset + 1] << 16) | ((uint)bytes[offset + 2] << 8) | bytes[offset + 3]; } + private static void WriteUInt32BigEndian(byte[] bytes, int offset, uint value) { bytes[offset] = (byte)(value >> 24); bytes[offset + 1] = (byte)(value >> 16); bytes[offset + 2] = (byte)(value >> 8); bytes[offset + 3] = (byte)value; } + private static void WriteChunk(Stream output, byte[] type, byte[] data) + { + var length = new byte[4]; WriteUInt32BigEndian(length, 0, (uint)data.Length); output.Write(length, 0, 4); output.Write(type, 0, 4); output.Write(data, 0, data.Length); + var crcBytes = new byte[4]; WriteUInt32BigEndian(crcBytes, 0, ComputeCrc32(type, 0, type.Length, data)); output.Write(crcBytes, 0, 4); + } + private static uint ComputeCrc32(byte[] data, int offset, int count) { return ComputeCrc32(data, offset, count, null); } + private static uint ComputeCrc32(byte[] first, int offset, int count, byte[] second) + { + uint crc = 0xffffffffu; + for (int i = 0; i < count; i++) crc = UpdateCrc(crc, first[offset + i]); + if (second != null) for (int i = 0; i < second.Length; i++) crc = UpdateCrc(crc, second[i]); + return crc ^ 0xffffffffu; + } + private static uint UpdateCrc(uint crc, byte value) + { + crc ^= value; + for (int bit = 0; bit < 8; bit++) crc = (crc & 1u) == 0u ? crc >> 1 : 0xedb88320u ^ (crc >> 1); + return crc; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs index 1aca9c6..a857d05 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs @@ -1,53 +1,29 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; - namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 路径平滑的公共配置;所有距离使用 m。 +/// Local G2 路径平滑管线共享配置;所有距离单位为 m,曲率相关限制使用 1/m。 public sealed class PathSmoothingConfiguration { - /// 创建带有安全默认值的平滑配置。 + /// 创建采用首版安全采样、碰撞步长、净空和曲率容差默认值的可编辑配置。 public PathSmoothingConfiguration() { - OutputSpacingMeters = 0.05d; + OutputSpacingMeters = 0.025d; MaximumCollisionCheckStepMeters = 0.025d; - MinimumClearanceReserveMeters = 0.02d; - SmoothingStrength = 1d; - AllowFallbackToCoarsePath = true; - RetryStrengthScales = new ReadOnlyCollection( - new List { 1d, 0.75d, 0.50d, 0.25d }); + MinimumClearanceReserveMeters = 0d; + CurvatureLimitRadiusToleranceMeters = 0.002d; } - /// 正式单算法入口使用的方法。 - public SmoothingMethod Method { get; set; } - - /// 输出路径的目标弧长采样间距,单位 m。 + /// 发布路径沿弧长的目标采样间距,单位 m;必须为有限正数。 public double OutputSpacingMeters { get; set; } - /// 扫掠碰撞检查的最大步长,单位 m。 + /// 连续车体扫掠复核允许的最大中心步长,单位 m;较大值会降低碰撞检查分辨率。 public double MaximumCollisionCheckStepMeters { get; set; } - /// 平滑候选必须在最小净空之外保留的额外余量,单位 m。 + /// 除无碰撞外还要求保留的最小额外净空,单位 m。 public double MinimumClearanceReserveMeters { get; set; } - /// 算法初始平滑强度。 - public double SmoothingStrength { get; set; } + /// 曲率上限复核时允许相对名义最小转弯半径的缩减容差,单位 m。 + public double CurvatureLimitRadiusToleranceMeters { get; set; } - /// 所有平滑尝试失败时是否允许发布经过复核的原粗路径。 - public bool AllowFallbackToCoarsePath { get; set; } - - /// 有限且严格递减的平滑强度重试比例。 - public IReadOnlyList RetryStrengthScales { get; } - - /// 三次 B 样条专用参数。 - public CubicBSplineOptions CubicBSpline { get; } = new CubicBSplineOptions(); - - /// 局部三次 Bézier 专用参数。 - public LocalCubicBezierOptions LocalCubicBezier { get; } = new LocalCubicBezierOptions(); - - /// 分段五次多项式专用参数。 - public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); - - /// 局部 G2 五次过渡专用参数。 + /// 局部 G2 五次过渡窗口与候选筛选选项;返回同一配置对象的可编辑子配置。 public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions(); } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs index 7f49d76..51fe12b 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs @@ -2,79 +2,35 @@ using System; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 一次平滑尝试的不可变诊断信息。 +/// 一次 Local G2 平滑尝试的不可变诊断快照,适用于成功、取消和失败结果。 public sealed class PathSmoothingDiagnostics { - /// 创建不含路径指标的默认诊断信息。 + /// 创建不可行、零耗时且没有终止原因的默认诊断快照。 public PathSmoothingDiagnostics() - : this(new PathQualityMetrics(), TimeSpan.Zero, 0, 0d, string.Empty) + : this(new PathQualityMetrics(), TimeSpan.Zero, string.Empty) { } - /// 创建完整的平滑诊断快照。 + /// 创建平滑诊断快照。 + /// 路径可行性、曲率、净空和相对变化的质量指标。 + /// 从服务入口到终止的累计耗时。 + /// 可供调用方记录的成功、取消或失败原因;为 时为空字符串。 public PathSmoothingDiagnostics( PathQualityMetrics metrics, TimeSpan elapsed, - int retryCount, - double acceptedStrength, string terminationReason = null) { Metrics = metrics ?? new PathQualityMetrics(); Elapsed = elapsed; - RetryCount = retryCount; - AcceptedStrength = acceptedStrength; TerminationReason = terminationReason ?? string.Empty; } - /// 使用所有测量量创建平滑诊断快照。 - public PathSmoothingDiagnostics( - bool isFeasible, - double pathLengthMeters, - double maximumAbsoluteVehicleCurvaturePerMeter, - double rootMeanSquareVehicleCurvaturePerMeter, - double totalAbsoluteCurvatureVariationPerMeter, - double curvatureVariationEnergy, - double minimumBodyClearanceMeters, - double lengthChangePercent, - double peakCurvatureChangePercent, - double curvatureVariationChangePercent, - double minimumClearanceChangeMeters, - TimeSpan elapsed, - int retryCount, - double acceptedStrength, - string terminationReason = null) - : this( - new PathQualityMetrics( - isFeasible, - pathLengthMeters, - maximumAbsoluteVehicleCurvaturePerMeter, - rootMeanSquareVehicleCurvaturePerMeter, - totalAbsoluteCurvatureVariationPerMeter, - curvatureVariationEnergy, - minimumBodyClearanceMeters, - lengthChangePercent, - peakCurvatureChangePercent, - curvatureVariationChangePercent, - minimumClearanceChangeMeters), - elapsed, - retryCount, - acceptedStrength, - terminationReason) - { - } - - /// 路径质量指标;始终非空。 + /// 本次尝试的质量指标;未提供时为全零且不可行的默认指标。 public PathQualityMetrics Metrics { get; } - /// 从算法入口到返回诊断的耗时。 + /// 本次尝试的累计耗时,类型为 public TimeSpan Elapsed { get; } - /// 已执行的安全强度重试次数。 - public int RetryCount { get; } - - /// 通过复核的平滑强度;未接受候选时为零。 - public double AcceptedStrength { get; } - - /// 面向调用方的稳定终止说明。 + /// 终止原因文本;不承载可消费的部分路径。 public string TerminationReason { get; } } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs index ebe8106..1b2f24a 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs @@ -5,13 +5,23 @@ using MultiWheelC.TrajectoryPlanning.Mapping; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 路径平滑所需的原始粗路径、复核上下文和配置。 +/// +/// 一次路径平滑所需的不可变输入快照。 +/// 粗路径、方向段、车辆和配置均在构造时复制;地图保留为调用方已冻结的 快照。 +/// public sealed class PathSmoothingRequest { private readonly VehicleParameters _vehicle; private readonly PathSmoothingConfiguration _configuration; - /// 创建路径平滑请求,并复制粗路径和方向分段集合。 + /// + /// 创建路径平滑请求,并防御性复制可变的路径、分段、车辆和配置输入。 + /// + /// 按起点到终点顺序排列的粗路径点集合;位置与弧长单位为 m,航向单位为 rad。 + /// 对应的前进/倒车包含式方向分段集合。 + /// 用于完整车体碰撞与净空复核的已准备不可变规划地图快照。 + /// 车辆长宽、安全余量和曲率约束;长度单位为 m,曲率单位为 1/m。 + /// 本次平滑的采样、净空、曲率和 Local G2 配置;构造后不会与调用方共享可变实例。 public PathSmoothingRequest( IReadOnlyList coarsePath, IReadOnlyList segments, @@ -26,13 +36,13 @@ public sealed class PathSmoothingRequest _configuration = CopyConfiguration(configuration); } - /// 原始粗路径的不可变快照。 + /// 原始粗路径的只读快照,按起点到终点顺序排列;位置与弧长单位为 m,航向单位为 rad。 public IReadOnlyList CoarsePath { get; } - /// 原始粗路径方向分段的不可变快照。 + /// 原始粗路径方向分段的只读快照;每段标识连续前进或倒车区间。 public IReadOnlyList Segments { get; } - /// 用于平滑后完整车体复核的规划栅格地图。 + /// 用于平滑后完整车体碰撞和净空复核的已冻结规划栅格地图快照。 public PlanningGridMap Map { get; } /// 车辆几何与最大曲率约束的不可变快照副本。 @@ -70,19 +80,11 @@ public sealed class PathSmoothingRequest if (source == null) return null; var copy = new PathSmoothingConfiguration { - Method = source.Method, OutputSpacingMeters = source.OutputSpacingMeters, MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters, MinimumClearanceReserveMeters = source.MinimumClearanceReserveMeters, - SmoothingStrength = source.SmoothingStrength, - AllowFallbackToCoarsePath = source.AllowFallbackToCoarsePath, + CurvatureLimitRadiusToleranceMeters = source.CurvatureLimitRadiusToleranceMeters, }; - copy.CubicBSpline.EndpointTangentScale = source.CubicBSpline.EndpointTangentScale; - copy.LocalCubicBezier.CornerHeadingThresholdRadians = source.LocalCubicBezier.CornerHeadingThresholdRadians; - copy.LocalCubicBezier.MaximumWindowLengthMeters = source.LocalCubicBezier.MaximumWindowLengthMeters; - copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio; - copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters; - copy.PiecewiseQuintic.MinimumKnotSpacingMeters = source.PiecewiseQuintic.MinimumKnotSpacingMeters; copy.LocalG2Quintic.MinimumWindowLengthMeters = source.LocalG2Quintic.MinimumWindowLengthMeters; copy.LocalG2Quintic.PreferredWindowLengthMeters = source.LocalG2Quintic.PreferredWindowLengthMeters; copy.LocalG2Quintic.MaximumWindowLengthMeters = source.LocalG2Quintic.MaximumWindowLengthMeters; diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs index d964caa..966adbe 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs @@ -4,7 +4,10 @@ using System.Collections.ObjectModel; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 路径平滑的最终不可变结果。 +/// +/// Local G2 路径平滑管线产生的不可变结果。 +/// 只有可发布状态携带完整、已验证的路径和方向段;失败、取消和无效输入结果始终提供空集合,不能被当作部分路径消费。 +/// public sealed class PathSmoothingResult { private static readonly IReadOnlyList EmptyPath = @@ -30,64 +33,34 @@ public sealed class PathSmoothingResult Diagnostics = diagnostics ?? new PathSmoothingDiagnostics( new PathQualityMetrics(), TimeSpan.Zero, - 0, - 0d, "No smoothing diagnostics were supplied."); } - /// 最终发布状态。 + /// 本次平滑的终止状态;决定 是否可消费。 public PathSmoothingStatus Status { get; } - /// 成功或回退时的实际(或尝试)平滑方法;失败时为空。 + /// 成功发布路径时实际采用的平滑方法;失败结果为 public SmoothingMethod? Method { get; } - /// 成功或经过复核的回退路径;其他状态始终为空且不可变。 + /// 成功时按起点到终点排列的不可变平滑路径;位置和弧长单位为 m,航向单位为 rad;失败时为空。 public IReadOnlyList Path { get; } - /// 覆盖 的方向分段;其他状态始终为空且不可变。 + /// 成功时覆盖 的不可变方向段集合;失败时为空。 public IReadOnlyList Segments { get; } - /// 局部 G2 各检测区域的不可变报告;传统算法结果为空。 + /// 各 Local G2 区域的不可变处理报告;失败结果不包含区域发布记录。 public IReadOnlyList RegionReports { get; } - /// 本次平滑的质量和终止诊断;始终非空。 + /// 质量指标、耗时和终止原因;始终存在,供调用方诊断成功或失败。 public PathSmoothingDiagnostics Diagnostics { get; } - /// 创建已通过所有复核的平滑结果。 - public static PathSmoothingResult Success( - SmoothingMethod method, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics) - { - ValidatePublishedResult(method, path, segments, diagnostics); - return new PathSmoothingResult( - PathSmoothingStatus.Success, - method, - CopyReadOnly(path), - CopyReadOnly(segments), - diagnostics, - EmptyRegionReports); - } - - /// 创建经过完整复核的原始粗路径回退结果。 - public static PathSmoothingResult Fallback( - SmoothingMethod attemptedMethod, - IReadOnlyList path, - IReadOnlyList segments, - PathSmoothingDiagnostics diagnostics) - { - ValidatePublishedResult(attemptedMethod, path, segments, diagnostics); - return new PathSmoothingResult( - PathSmoothingStatus.FallbackToCoarsePath, - attemptedMethod, - CopyReadOnly(path), - CopyReadOnly(segments), - diagnostics, - EmptyRegionReports); - } - - /// 发布经过完整复核的局部 G2 预平滑结果。 + /// 发布已经过独立几何、曲率、净空和连续碰撞复核的 Local G2 结果。 + /// 可发布状态,只能是完整、部分改进、不需要平滑或保持原样之一。 + /// 按起点到终点顺序排列的完整平滑路径;位置与弧长单位为 m,航向单位为 rad。 + /// 覆盖完整路径的前进/倒车方向段集合。 + /// 包含可行质量指标和终止说明的诊断快照。 + /// 每个局部区域的不可变处理报告集合,不能为 。 + /// 携带防御性复制路径、分段和区域报告的不可变可消费结果。 public static PathSmoothingResult PublishLocalG2( PathSmoothingStatus status, IReadOnlyList path, @@ -99,11 +72,13 @@ public sealed class PathSmoothingResult status != PathSmoothingStatus.PartialImprovement && status != PathSmoothingStatus.NotNeeded && status != PathSmoothingStatus.Unchanged) + { throw new ArgumentException("Use a Local G2 publication status.", nameof(status)); + } if (regionReports == null) throw new ArgumentNullException(nameof(regionReports)); - ValidatePublishedResult(SmoothingMethod.LocalG2Quintic, path, segments, diagnostics); + ValidatePublishedResult(path, segments, diagnostics); return new PathSmoothingResult( status, SmoothingMethod.LocalG2Quintic, @@ -113,29 +88,29 @@ public sealed class PathSmoothingResult CopyReadOnly(regionReports)); } - /// 创建不发布路径的失败、不可行、取消或输入无效结果。 + /// 创建明确不发布路径的失败、取消或无效输入结果。 + /// 非可发布的终止状态。 + /// 失败原因、质量指标和耗时;为 时生成默认诊断。 + /// 路径、方向段和区域报告均为空的不可变结果,不能作为部分路径使用。 public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics) { - if (status == PathSmoothingStatus.Success || - status == PathSmoothingStatus.FallbackToCoarsePath || - status == PathSmoothingStatus.Complete || + if (status == PathSmoothingStatus.Complete || status == PathSmoothingStatus.PartialImprovement || status == PathSmoothingStatus.NotNeeded || status == PathSmoothingStatus.Unchanged) - throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status)); + { + throw new ArgumentException("Use PublishLocalG2 to publish a path.", nameof(status)); + } if (!Enum.IsDefined(typeof(PathSmoothingStatus), status)) throw new ArgumentOutOfRangeException(nameof(status)); return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics, EmptyRegionReports); } private static void ValidatePublishedResult( - SmoothingMethod method, IReadOnlyList path, IReadOnlyList segments, PathSmoothingDiagnostics diagnostics) { - if (!Enum.IsDefined(typeof(SmoothingMethod), method)) - throw new ArgumentOutOfRangeException(nameof(method)); if (path == null || path.Count == 0) throw new ArgumentException("Published smoothing results require a non-empty path.", nameof(path)); if (segments == null || segments.Count == 0) @@ -147,8 +122,7 @@ public sealed class PathSmoothingResult private static IReadOnlyList CopyReadOnly(IReadOnlyList source) { var copy = new List(source.Count); - for (int index = 0; index < source.Count; index++) - copy.Add(source[index]); + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); return new ReadOnlyCollection(copy); } } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs index df1cdab..bac1baf 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs @@ -1,10 +1,8 @@ namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 路径平滑的最终发布状态。 +/// 路径平滑的最终发布状态;只有 Complete、PartialImprovement、NotNeeded 和 Unchanged 可携带完整路径。 public enum PathSmoothingStatus { - Success, - FallbackToCoarsePath, InvalidInput, Infeasible, Failed, diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs index 9469fc6..c4eaed8 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs @@ -1,11 +1,14 @@ namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 平滑路径点的来源。 +/// 平滑路径点的来源;用于区分保留锚点、常规重采样、换向锚点和 Local G2 过渡几何。 public enum SmoothedPathPointSource { + /// 直接保留的原始路径锚点。 Anchor, + /// 在同一方向段内按弧长插值得到的常规采样点。 Interpolated, + /// 精确保留的换向边界点;不能与相邻方向段按坐标合并。 GearSwitch, - CoarsePathFallback, - LocalG2Transition, + /// 由 Local G2 五次过渡曲线生成的候选或发布点。 + LocalG2Transition = 4, } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs index 890e02f..b2bc5b1 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs @@ -1,10 +1,7 @@ namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; -/// 支持的粗路径平滑方法。 +/// 本模块可以发布的路径平滑方法枚举;当前仅支持 Local G2 五次过渡。 public enum SmoothingMethod { - CubicBSpline, - LocalCubicBezier, - PiecewiseQuintic, LocalG2Quintic, } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs index 5a34d9a..c0ed61b 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs @@ -2,13 +2,16 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -/// 以固定预热和五次测量隔离比较所有请求平滑方法的离线入口。 +/// +/// 原始路径基线与一次 Local G2 平滑的离线比较入口。 +/// 此类仅汇总已冻结请求的质量、确定性摘要和耗时,不参与实时控制或修改平滑结果。 +/// public sealed class PathSmoothingComparisonService { private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); @@ -16,13 +19,16 @@ public sealed class PathSmoothingComparisonService private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); - /// 比较所有请求方法;一个方法的失败不会阻止其他方法,取消会停止后续启动。 + /// 创建原始基线并比较一次 Local G2 运行的质量、耗时和推荐资格。 + /// 比较请求,包含待平滑的不可变请求快照和比较设置;为 时返回带失败基线的结果。 + /// 取消比较和重复计时的调用方令牌。 + /// 包含原始基线、候选条目和推荐方法的不可变比较结果;取消时 Cancelled public PathSmoothingComparisonResult Compare( PathSmoothingComparisonRequest request, CancellationToken cancellationToken = default) { PathSmoothingComparisonEntry baseline = CreateRawPathBaseline(request, out string baselineReason); - var entries = new List(); + var entries = new List(1); if (cancellationToken.IsCancellationRequested) return Cancelled(baseline, entries); @@ -30,33 +36,26 @@ public sealed class PathSmoothingComparisonService if (request == null || request.SmoothingRequest == null) return new PathSmoothingComparisonResult(baseline, entries, null, false, baselineReason); - for (int methodIndex = 0; methodIndex < request.Methods.Count; methodIndex++) + if (!TryCompareLocalG2( + request.SmoothingRequest, + baseline.Metrics, + cancellationToken, + out PathSmoothingComparisonEntry entry)) { - if (cancellationToken.IsCancellationRequested) - return Cancelled(baseline, entries); - - SmoothingMethod method = request.Methods[methodIndex]; - if (!TryCompareMethod( - request.SmoothingRequest, - method, - baseline.Metrics, - cancellationToken, - out PathSmoothingComparisonEntry entry)) - return Cancelled(baseline, entries); - entries.Add(entry); + return Cancelled(baseline, entries); } + entries.Add(entry); return new PathSmoothingComparisonResult( baseline, entries, - SmoothingMethodRanker.Rank(entries), + entry.IsEligibleForRecommendation ? SmoothingMethod.LocalG2Quintic : null, false, baselineReason); } - private bool TryCompareMethod( - PathSmoothingRequest sourceRequest, - SmoothingMethod method, + private bool TryCompareLocalG2( + PathSmoothingRequest smoothingRequest, PathQualityMetrics rawMetrics, CancellationToken cancellationToken, out PathSmoothingComparisonEntry entry) @@ -64,9 +63,8 @@ public sealed class PathSmoothingComparisonService entry = null; try { - PathSmoothingRequest methodRequest = CreateMethodRequest(sourceRequest, method); cancellationToken.ThrowIfCancellationRequested(); - PathSmoothingResult warmup = _smoothingService.Smooth(methodRequest, cancellationToken); + PathSmoothingResult warmup = _smoothingService.Smooth(smoothingRequest, cancellationToken); if (warmup.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; var timings = new List(5); @@ -75,7 +73,7 @@ public sealed class PathSmoothingComparisonService { cancellationToken.ThrowIfCancellationRequested(); Stopwatch stopwatch = Stopwatch.StartNew(); - PathSmoothingResult result = _smoothingService.Smooth(methodRequest, cancellationToken); + PathSmoothingResult result = _smoothingService.Smooth(smoothingRequest, cancellationToken); stopwatch.Stop(); if (result.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; timings.Add(stopwatch.Elapsed.TotalMilliseconds); @@ -88,12 +86,11 @@ public sealed class PathSmoothingComparisonService string diagnostic = string.IsNullOrWhiteSpace(timing.Diagnostic) ? canonical.Diagnostics.TerminationReason : timing.Diagnostic; - - PathQualityMetrics metrics = canonical.Status == PathSmoothingStatus.Success + PathQualityMetrics metrics = PathSmoothingComparisonEntry.IsPublishedLocalG2Status(canonical.Status) ? NormalizeMetrics(canonical.Diagnostics.Metrics, rawMetrics) : new PathQualityMetrics(); entry = PathSmoothingComparisonEntry.CreateCandidate( - method, + SmoothingMethod.LocalG2Quintic, canonical.Status, metrics, timing, @@ -110,7 +107,7 @@ public sealed class PathSmoothingComparisonService catch (Exception exception) { entry = PathSmoothingComparisonEntry.CreateCandidate( - method, + SmoothingMethod.LocalG2Quintic, PathSmoothingStatus.Failed, new PathQualityMetrics(), new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, exception.GetType().Name), @@ -128,12 +125,17 @@ public sealed class PathSmoothingComparisonService { reason = string.Empty; if (request == null || request.SmoothingRequest == null) - return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求为空。", out reason); + return FailedBaseline(PathSmoothingStatus.InvalidInput, "Comparison request is required.", out reason); PathSmoothingRequest smoothingRequest = request.SmoothingRequest; PathSmoothingConfiguration configuration = smoothingRequest.Configuration; if (configuration == null || smoothingRequest.Map == null || smoothingRequest.Vehicle == null) - return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求缺少可用的地图、车辆或配置。", out reason); + { + return FailedBaseline( + PathSmoothingStatus.InvalidInput, + "A planning-ready map, vehicle, and configuration are required.", + out reason); + } if (!_preprocessor.TryPrepare(smoothingRequest, out PreparedPath preparedPath, out reason)) return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); @@ -146,11 +148,13 @@ public sealed class PathSmoothingComparisonService configuration.MaximumCollisionCheckStepMeters, out RawPathBaseline rawPath, out reason)) + { return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + } - string digest = StableGeometryDigest.Compute(PathSmoothingStatus.Success, null, rawPath.Path, rawPath.Segments); + string digest = StableGeometryDigest.Compute(PathSmoothingStatus.NotNeeded, null, rawPath.Path, rawPath.Segments); return PathSmoothingComparisonEntry.CreateRawPathBaseline( - PathSmoothingStatus.Success, + PathSmoothingStatus.NotNeeded, rawPath.Metrics, digest, string.Empty, @@ -177,20 +181,7 @@ public sealed class PathSmoothingComparisonService PathSmoothingComparisonEntry baseline, IReadOnlyList entries) { - return new PathSmoothingComparisonResult(baseline, entries, null, true, "路径平滑比较已取消。"); - } - - private static PathSmoothingRequest CreateMethodRequest(PathSmoothingRequest source, SmoothingMethod method) - { - PathSmoothingConfiguration configuration = source.Configuration; - configuration.Method = method; - configuration.AllowFallbackToCoarsePath = false; - return new PathSmoothingRequest( - source.CoarsePath, - source.Segments, - source.Map, - source.Vehicle, - configuration); + return new PathSmoothingComparisonResult(baseline, entries, null, true, "Path smoothing comparison was cancelled."); } private static PathQualityMetrics NormalizeMetrics( diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs index d31614d..f8bf4ea 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs @@ -1,28 +1,30 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; -/// 正式单算法路径平滑入口,负责输入校验、有限重试与经过复核的粗路径回退。 +/// +/// Local G2 路径平滑的公开、同步业务入口。 +/// 它只消费调用方冻结的请求,依次执行输入校验、预处理、原始基线、局部 G2 和独立复核;从不发布未经验证的部分路径。 +/// public sealed class PathSmoothingService { private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); - private readonly SmoothingAlgorithmRunner _runner = new SmoothingAlgorithmRunner(); private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); - private readonly IPathSmoother _bSpline = new CubicBSplineSmoother(); - private readonly IPathSmoother _bezier = new LocalCubicBezierSmoother(); - private readonly IPathSmoother _quintic = new PiecewiseQuinticSmoother(); + private readonly LocalG2PreSmoothingPipeline _localG2Pipeline = new LocalG2PreSmoothingPipeline(); - /// 执行一次经过完整安全复核的单算法平滑。 + /// 在请求携带的不可变粗路径、地图和车辆快照上执行一次 Local G2 平滑。 + /// 平滑输入快照,包含粗路径/方向段、规划地图、车辆和配置;路径单位为 m/rad,曲率单位为 1/m。 + /// 调用方取消令牌;取消时返回 ,不会发布部分路径。 + /// 成功时携带完整独立复核路径的结果;输入、预处理、碰撞、曲率或运行失败时路径和分段为空,原因位于诊断中。 public PathSmoothingResult Smooth( PathSmoothingRequest request, CancellationToken cancellationToken = default) @@ -32,10 +34,10 @@ public sealed class PathSmoothingService { cancellationToken.ThrowIfCancellationRequested(); if (!TryValidateRequest(request, out PathSmoothingConfiguration configuration, out string reason)) - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, reason); if (!_preprocessor.TryPrepare(request, out PreparedPath preparedPath, out reason)) - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, reason); if (!RawPathBaselineBuilder.TryCreate( request, @@ -44,126 +46,24 @@ public sealed class PathSmoothingService configuration.OutputSpacingMeters, _validator, configuration.MaximumCollisionCheckStepMeters, - out _, + out RawPathBaseline rawBaseline, out reason)) { - return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason); + return Failure(PathSmoothingStatus.InvalidInput, stopwatch, reason); } - cancellationToken.ThrowIfCancellationRequested(); - var input = new SmoothingAlgorithmInput( - preparedPath, - request.Map, - request.Vehicle, - configuration.MaximumCollisionCheckStepMeters, - configuration.MinimumClearanceReserveMeters, - new SmoothingOptionsSnapshot(configuration)); - SmoothingAlgorithmRunner.AlgorithmRunResult runResult = _runner.Run( - Resolve(configuration.Method), input, configuration, cancellationToken); - int retryCount = GetRetryCount(runResult.AttemptedStrengths); - PathSmoothingDiagnostics diagnostics = new PathSmoothingDiagnostics( - runResult.Metrics, - stopwatch.Elapsed, - retryCount, - runResult.AcceptedStrength, - runResult.Reason); - - if (runResult.Status == PathSmoothingStatus.Success) - { - return PathSmoothingResult.Success( - configuration.Method, - runResult.Path, - runResult.Segments, - diagnostics); - } - - if (!configuration.AllowFallbackToCoarsePath) - return PathSmoothingResult.Failure(runResult.Status, diagnostics); - - cancellationToken.ThrowIfCancellationRequested(); - if (!TryCreateVerifiedFallback( - request, - configuration, - cancellationToken, - out IReadOnlyList fallbackPath, - out IReadOnlyList fallbackSegments, - out PathQualityMetrics fallbackMetrics, - out reason)) - { - return PathSmoothingResult.Failure(runResult.Status, diagnostics); - } - - var fallbackDiagnostics = new PathSmoothingDiagnostics( - fallbackMetrics, - stopwatch.Elapsed, - retryCount, - runResult.AcceptedStrength, - runResult.Reason); - return PathSmoothingResult.Fallback( - configuration.Method, - fallbackPath, - fallbackSegments, - fallbackDiagnostics); + return _localG2Pipeline.Smooth(request, preparedPath, rawBaseline, cancellationToken); } catch (OperationCanceledException) { - return Failure(PathSmoothingStatus.Cancelled, stopwatch, 0, 0d, "路径平滑已取消。"); + return Failure(PathSmoothingStatus.Cancelled, stopwatch, "Path smoothing was cancelled."); } catch (Exception exception) { - return Failure(PathSmoothingStatus.Failed, stopwatch, 0, 0d, exception.Message); + return Failure(PathSmoothingStatus.Failed, stopwatch, exception.Message); } } - private bool TryCreateVerifiedFallback( - PathSmoothingRequest request, - PathSmoothingConfiguration configuration, - CancellationToken cancellationToken, - out IReadOnlyList fallbackPath, - out IReadOnlyList fallbackSegments, - out PathQualityMetrics fallbackMetrics, - out string reason) - { - fallbackPath = null; - fallbackSegments = null; - fallbackMetrics = null; - reason = string.Empty; - - // Reprepare from the immutable request instead of reusing the algorithm input: fallback is a - // separately published output and must repeat the coarse-path contract validation. - if (!_preprocessor.TryPrepare(request, out PreparedPath revalidatedPath, out reason)) return false; - cancellationToken.ThrowIfCancellationRequested(); - - if (!RawPathBaselineBuilder.TryCreate( - request, - revalidatedPath, - _analyzer, - configuration.OutputSpacingMeters, - _validator, - configuration.MaximumCollisionCheckStepMeters, - out RawPathBaseline rawPath, - out reason)) - { - return false; - } - - fallbackPath = ToFallbackPoints(rawPath.Path); - fallbackSegments = rawPath.Segments; - fallbackMetrics = rawPath.Metrics; - return true; - } - - private IPathSmoother Resolve(SmoothingMethod method) - { - return method switch - { - SmoothingMethod.CubicBSpline => _bSpline, - SmoothingMethod.LocalCubicBezier => _bezier, - SmoothingMethod.PiecewiseQuintic => _quintic, - _ => throw new ArgumentOutOfRangeException(nameof(method)), - }; - } - private static bool TryValidateRequest( PathSmoothingRequest request, out PathSmoothingConfiguration configuration, @@ -173,7 +73,7 @@ public sealed class PathSmoothingService reason = string.Empty; if (request == null) { - reason = "平滑请求为空。"; + reason = "Path smoothing request is required."; return false; } @@ -181,7 +81,7 @@ public sealed class PathSmoothingService VehicleParameters vehicle = request.Vehicle; if (configuration == null || request.Map == null || !request.Map.PlanningReady || vehicle == null) { - reason = "平滑请求缺少可用的地图、车辆或配置。"; + reason = "A planning-ready map, vehicle, and configuration are required."; return false; } @@ -190,13 +90,7 @@ public sealed class PathSmoothingService !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out _)) { - reason = "平滑请求中的车辆几何或曲率约束无效。"; - return false; - } - - if (!Enum.IsDefined(typeof(SmoothingMethod), configuration.Method)) - { - reason = "平滑方法无效。"; + reason = "Vehicle geometry or curvature constraints are invalid."; return false; } @@ -204,63 +98,48 @@ public sealed class PathSmoothingService !NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || !NumericGuard.IsFinite(configuration.MinimumClearanceReserveMeters) || configuration.MinimumClearanceReserveMeters < 0d || - !NumericGuard.IsPositiveFinite(configuration.SmoothingStrength) || - !NumericGuard.IsPositiveFinite(configuration.CubicBSpline.EndpointTangentScale) || - !IsValidBezierThreshold(configuration.LocalCubicBezier.CornerHeadingThresholdRadians) || - !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.MaximumWindowLengthMeters) || - !NumericGuard.IsPositiveFinite(configuration.LocalCubicBezier.HandleLengthRatio) || - !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.KnotSpacingMeters) || - !NumericGuard.IsPositiveFinite(configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) || - configuration.PiecewiseQuintic.KnotSpacingMeters < configuration.PiecewiseQuintic.MinimumKnotSpacingMeters) + !CurvatureLimitPolicy.TryGetAllowedMaximumVehicleCurvaturePerMeter( + vehicle, configuration.CurvatureLimitRadiusToleranceMeters, out _)) { - reason = "平滑配置包含非法数值或不满足方法契约。"; + reason = "Shared Local G2 configuration values are invalid."; return false; } - return true; + + return IsValidLocalG2Options(configuration.LocalG2Quintic, out reason); } - private static bool IsValidBezierThreshold(double thresholdRadians) + private static bool IsValidLocalG2Options(LocalG2QuinticOptions options, out string reason) { - return NumericGuard.IsFinite(thresholdRadians) && thresholdRadians > 0d && thresholdRadians <= Math.PI; - } - - private static IReadOnlyList ToFallbackPoints(IReadOnlyList path) - { - var points = new List(path.Count); - for (int index = 0; index < path.Count; index++) + reason = string.Empty; + if (options != null && + NumericGuard.IsPositiveFinite(options.MinimumWindowLengthMeters) && + NumericGuard.IsFinite(options.PreferredWindowLengthMeters) && + options.PreferredWindowLengthMeters >= options.MinimumWindowLengthMeters && + NumericGuard.IsFinite(options.MaximumWindowLengthMeters) && + options.MaximumWindowLengthMeters >= options.PreferredWindowLengthMeters && + NumericGuard.IsPositiveFinite(options.MaximumDeviationMeters) && + NumericGuard.IsPositiveFinite(options.AbsoluteCurvatureJumpFloorPerMeter) && + NumericGuard.IsFinite(options.CurvatureJumpRatioOfMaximum) && + options.CurvatureJumpRatioOfMaximum > 0d && options.CurvatureJumpRatioOfMaximum <= 1d && + NumericGuard.IsFinite(options.MinimumPeakGradientImprovementRatio) && + options.MinimumPeakGradientImprovementRatio > 0d && options.MinimumPeakGradientImprovementRatio < 1d && + NumericGuard.IsFinite(options.MaximumVariationCostRegressionRatio) && + options.MaximumVariationCostRegressionRatio >= 0d && options.MaximumCandidatesPerRegion >= 1) { - SmoothedPathPoint point = path[index]; - points.Add(new SmoothedPathPoint( - point.X, - point.Y, - point.Heading, - point.UnwrappedHeading, - point.ArcLength, - point.Direction, - point.GeometricCurvature, - point.VehicleCurvature, - point.VehicleCurvatureDerivative, - point.BodyClearance, - point.IsGearSwitchPoint, - SmoothedPathPointSource.CoarsePathFallback)); + return true; } - return points; - } - private static int GetRetryCount(IReadOnlyList attemptedStrengths) - { - return attemptedStrengths == null || attemptedStrengths.Count == 0 ? 0 : attemptedStrengths.Count - 1; + reason = "Local G2 configuration values are invalid."; + return false; } private static PathSmoothingResult Failure( PathSmoothingStatus status, Stopwatch stopwatch, - int retryCount, - double acceptedStrength, string reason) { return PathSmoothingResult.Failure( status, - new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, retryCount, acceptedStrength, reason)); + new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, reason)); } } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs index 50b84d5..fc00d04 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs @@ -12,7 +12,7 @@ using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; -/// 对一个局部 G2 替换候选执行区域质量门和完整路径安全复核。 +/// 对局部 G2 替换候选执行区域质量门、整条路径几何重算及碰撞安全复核。 internal sealed class LocalG2CandidateEvaluator { private const double CurvatureRangeTolerance = 1e-6d; @@ -25,11 +25,16 @@ internal sealed class LocalG2CandidateEvaluator private readonly LocalG2PathSplicer _splicer; private readonly SmoothedPathValidator _validator; + /// 使用默认几何分析、路径拼接和安全校验器创建候选评估器。 internal LocalG2CandidateEvaluator() : this(new PathGeometryAnalyzer(), new LocalG2PathSplicer(), new SmoothedPathValidator()) { } + /// 使用注入的依赖创建候选评估器,便于复用统一的几何与碰撞判定规则。 + /// 将预处理路径转换为带曲率指标的 的分析器。 + /// 将候选窗口替换回当前路径并维持换向拓扑的拼接器。 + /// 对完整候选路径执行碰撞、净空和轨迹不变量检查的校验器。 internal LocalG2CandidateEvaluator( PathGeometryAnalyzer analyzer, LocalG2PathSplicer splicer, @@ -40,6 +45,15 @@ internal sealed class LocalG2CandidateEvaluator _validator = validator ?? throw new ArgumentNullException(nameof(validator)); } + /// 评估一个局部 G2 候选,仅在全部质量门与完整路径安全复核通过时接受。 + /// 未经局部替换的基线路径;用于约束曲率范围和总长度变化。 + /// 已接受先前窗口替换的当前路径;本候选将在其指定方向段内拼接。 + /// 候选所属的单一行驶方向平滑区域。 + /// 待评估候选的窗口边界与几何点;坐标和弧长单位为 m。 + /// 平滑请求,提供地图、车辆模型、采样间距 m 和安全阈值。 + /// 局部 G2 质量门选项,包括偏差 m、曲率导数 1/m² 和代价回退阈值。 + /// 取消令牌;取消时抛出 。 + /// 包含接受状态、失败原因和度量值的 ;拒绝时不发布候选路径。 internal LocalG2CandidateEvaluation Evaluate( PreparedPath rawPath, PreparedPath currentPath, @@ -67,7 +81,9 @@ internal sealed class LocalG2CandidateEvaluator string.IsNullOrEmpty(reason) ? "局部 G2 区域几何分析失败。" : reason); } - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double vehicleMaximumCurvature)) + if (!CurvatureLimitPolicy.TryGetAllowedMaximumVehicleCurvaturePerMeter( + request.Vehicle, request.Configuration.CurvatureLimitRadiusToleranceMeters, + out double vehicleMaximumCurvature)) return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CandidateGenerationFailed, "车辆曲率约束无效。"); if (candidateAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter > vehicleMaximumCurvature + CurvatureRangeTolerance) return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureExceeded, "局部 G2 候选超过车辆曲率上限。"); @@ -91,7 +107,9 @@ internal sealed class LocalG2CandidateEvaluator } if (!_validator.TryValidate(fullAnalysis.Path, fullAnalysis.Segments, rawPath, request.Map, request.Vehicle, - request.Configuration.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath, + request.Configuration.MaximumCollisionCheckStepMeters, + request.Configuration.CurvatureLimitRadiusToleranceMeters, + out IReadOnlyList safePath, out double minimumClearance, out reason)) { return Rejected(candidateIndex, PathSmoothingRegionFailureReason.Collision, @@ -145,6 +163,9 @@ internal sealed class LocalG2CandidateEvaluator "Accepted"); } + /// 在已通过质量门的候选中按偏差、峰值曲率导数、变化代价、长度变化和索引稳定选优。 + /// 待比较的候选评估结果集合;仅 Accepted 的项可入选。 + /// 最优的已接受评估;若没有合格项,返回带失败原因的拒绝评估。 internal static LocalG2CandidateEvaluation SelectBest(IReadOnlyList evaluations) { if (evaluations == null) throw new ArgumentNullException(nameof(evaluations)); diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs new file mode 100644 index 0000000..ff8c6c8 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; + +/// 通过专用服务路径生成、回滚并发布经独立验证的 Local G2 局部区域替换;不发布未经最终复核的候选。 +internal sealed class LocalG2PreSmoothingPipeline +{ + private readonly CurvatureTransitionDetector _detector = new CurvatureTransitionDetector(); + private readonly LocalG2WindowPlanner _windowPlanner = new LocalG2WindowPlanner(); + private readonly LocalG2CandidateBuilder _builder = new LocalG2CandidateBuilder(); + private readonly LocalG2CandidateEvaluator _evaluator = new LocalG2CandidateEvaluator(); + private readonly LocalG2RegionWorkOrder _workOrder = new LocalG2RegionWorkOrder(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); + + /// 在预处理路径上检测曲率事件、评估候选、按确定顺序拼接,并执行全局回滚复核。 + /// 不可变平滑请求,提供地图、车辆、配置和路径上下文。 + /// 按方向段准备并重采样的原始路径;局部弧长单位为 m。 + /// 已独立复核的原始路径质量基线。 + /// 调用方取消令牌;取消时返回空路径的 Cancelled 结果。 + /// 完整、部分、无需或保持原样时均只发布最终复核路径;失败/取消时不发布部分候选。 + internal PathSmoothingResult Smooth( + PathSmoothingRequest request, + PreparedPath preparedPath, + RawPathBaseline rawBaseline, + CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + if (request == null || preparedPath == null || rawBaseline == null || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(request.Vehicle, out double maximumCurvature)) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, "局部 G2 预平滑输入无效。"); + } + + var options = new LocalG2OptionsSnapshot(request.Configuration); + if (!_detector.TryDetect(request, maximumCurvature, options, out IReadOnlyList transitions, out string reason) || + !_windowPlanner.TryPlan(preparedPath, transitions, options, out IReadOnlyList regions, out reason)) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, reason); + } + + IReadOnlyList reportOrder = + new ReadOnlyCollection(new List(regions)); + if (!_workOrder.TryCreate(reportOrder, out IReadOnlyList workRegions, out string orderReason)) + return Failure(PathSmoothingStatus.Failed, stopwatch, orderReason); + + PreparedPath current = preparedPath; + var reportsByRegion = new Dictionary(); + var accepted = new List(); + int improvedCount = 0; + + foreach (LocalG2SmoothingRegion region in workRegions) + { + cancellationToken.ThrowIfCancellationRequested(); + IReadOnlyList candidates = _builder.Build( + preparedPath.Segments[region.SegmentIndex], region, + request.Configuration.OutputSpacingMeters, options, cancellationToken); + var evaluations = new List(); + for (int candidateIndex = 0; candidateIndex < candidates.Count; candidateIndex++) + evaluations.Add(_evaluator.Evaluate( + preparedPath, current, region, candidates[candidateIndex], request, options, cancellationToken)); + + LocalG2CandidateEvaluation best = LocalG2CandidateEvaluator.SelectBest(evaluations); + if (best.Accepted) + { + accepted.Add(new AcceptedRegion(region, current, best)); + current = best.SplicedPreparedPath; + improvedCount++; + reportsByRegion.Add(region, CreateImprovedReport(region, candidates.Count, best)); + } + else + { + reportsByRegion.Add(region, CreateRetainedReport(region, candidates.Count, best)); + } + } + + if (!TryValidateFinal(current, preparedPath, rawBaseline, request, options, out IReadOnlyList path, + out IReadOnlyList segments, out PathQualityMetrics metrics, out reason)) + { + for (int index = accepted.Count - 1; index >= 0; index--) + { + AcceptedRegion rollback = accepted[index]; + current = rollback.Before; + reportsByRegion[rollback.Region] = CreateRollbackReport(rollback.Region, rollback.Evaluation); + improvedCount--; + if (TryValidateFinal(current, preparedPath, rawBaseline, request, options, out path, out segments, out metrics, out reason)) + break; + } + } + + if (metrics == null) + return Failure(PathSmoothingStatus.Failed, stopwatch, reason); + + var reports = new List(reportOrder.Count); + for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++) + reports.Add(reportsByRegion[reportOrder[reportIndex]]); + + PathSmoothingStatus status; + if (transitions.Count == 0) status = PathSmoothingStatus.NotNeeded; + else if (improvedCount == regions.Count) status = PathSmoothingStatus.Complete; + else if (improvedCount > 0) status = PathSmoothingStatus.PartialImprovement; + else status = PathSmoothingStatus.Unchanged; + return PathSmoothingResult.PublishLocalG2( + status, + path, + segments, + new PathSmoothingDiagnostics(metrics, stopwatch.Elapsed, reason ?? string.Empty), + reports); + } + catch (OperationCanceledException) + { + return Failure(PathSmoothingStatus.Cancelled, stopwatch, "路径平滑已取消。"); + } + catch (Exception exception) + { + return Failure(PathSmoothingStatus.Failed, stopwatch, exception.Message); + } + } + + private bool TryValidateFinal( + PreparedPath current, + PreparedPath rawPath, + RawPathBaseline rawBaseline, + PathSmoothingRequest request, + LocalG2OptionsSnapshot options, + out IReadOnlyList path, + out IReadOnlyList segments, + out PathQualityMetrics metrics, + out string reason) + { + path = null; + segments = null; + metrics = null; + reason = string.Empty; + if (!_analyzer.TryAnalyze(current.Segments, request.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason) || + !_validator.TryValidate(analysis.Path, analysis.Segments, rawPath, request.Map, request.Vehicle, + request.Configuration.MaximumCollisionCheckStepMeters, + request.Configuration.CurvatureLimitRadiusToleranceMeters, + out IReadOnlyList safePath, + out double minimumClearance, out reason) || + minimumClearance < request.Configuration.MinimumClearanceReserveMeters || + analysis.CurvatureVariationCost > rawBaseline.Metrics.CurvatureVariationCost * + (1d + options.MaximumVariationCostRegressionRatio)) + { + if (string.IsNullOrEmpty(reason)) reason = "局部 G2 完整路径复核未通过。"; + return false; + } + + path = safePath; + segments = analysis.Segments; + metrics = new PathQualityMetrics( + true, + analysis.PathLengthMeters, + analysis.MaximumAbsoluteVehicleCurvaturePerMeter, + analysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter, + analysis.RootMeanSquareVehicleCurvaturePerMeter, + analysis.TotalAbsoluteCurvatureVariationPerMeter, + analysis.CurvatureVariationCost, + minimumClearance, + 0d, 0d, 0d, 0d); + return true; + } + + private static PathSmoothingRegionReport CreateImprovedReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.Improved, PathSmoothingRegionFailureReason.None); + + private static PathSmoothingRegionReport CreateRetainedReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, candidateCount, evaluation, PathSmoothingRegionStatus.RetainedOriginal, evaluation.FailureReason); + + private static PathSmoothingRegionReport CreateRollbackReport( + LocalG2SmoothingRegion region, + LocalG2CandidateEvaluation evaluation) => + CreateReport(region, region.WindowVariants.Count, evaluation, PathSmoothingRegionStatus.RetainedOriginal, + PathSmoothingRegionFailureReason.GlobalValidationRollback); + + private static PathSmoothingRegionReport CreateReport( + LocalG2SmoothingRegion region, + int candidateCount, + LocalG2CandidateEvaluation evaluation, + PathSmoothingRegionStatus status, + PathSmoothingRegionFailureReason failureReason) + { + LocalG2WindowVariant window = region.WindowVariants[0]; + var jumps = new List(region.Transitions.Count); + for (int index = 0; index < region.Transitions.Count; index++) + jumps.Add(region.Transitions[index].RightVehicleCurvaturePerMeter - region.Transitions[index].LeftVehicleCurvaturePerMeter); + return new PathSmoothingRegionReport( + region.SegmentIndex, + window.StartArcLengthMeters, + window.EndArcLengthMeters, + jumps, + window.EndArcLengthMeters - window.StartArcLengthMeters, + window.EndArcLengthMeters - window.StartArcLengthMeters, + window.LeftWindowLengthMeters, + window.RightWindowLengthMeters, + candidateCount, + evaluation.CandidateIndex, + status, + failureReason, + evaluation.RawPeakCurvatureDerivativePerSquareMeter, + evaluation.ResultPeakCurvatureDerivativePerSquareMeter, + evaluation.RawCurvatureVariationCost, + evaluation.ResultCurvatureVariationCost, + evaluation.MaximumDeviationMeters, + evaluation.MinimumBodyClearanceMeters, + evaluation.MaximumAbsoluteVehicleCurvaturePerMeter); + } + + private static PathSmoothingResult Failure(PathSmoothingStatus status, Stopwatch stopwatch, string reason) => + PathSmoothingResult.Failure(status, + new PathSmoothingDiagnostics(new PathQualityMetrics(), stopwatch.Elapsed, reason)); + + private sealed class AcceptedRegion + { + internal AcceptedRegion(LocalG2SmoothingRegion region, PreparedPath before, LocalG2CandidateEvaluation evaluation) + { + Region = region; + Before = before; + Evaluation = evaluation; + } + + internal LocalG2SmoothingRegion Region { get; } + internal PreparedPath Before { get; } + internal LocalG2CandidateEvaluation Evaluation { get; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonEntry.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonEntry.cs new file mode 100644 index 0000000..2e951fd --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonEntry.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +/// Immutable raw-baseline or Local G2 report entry. +public sealed class PathSmoothingComparisonEntry +{ + public PathSmoothingComparisonEntry( + SmoothingMethod method, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic) + : this(method, false, status, metrics, timing, stableGeometryDigest, diagnostic, Empty(), Empty()) + { + } + + private PathSmoothingComparisonEntry( + SmoothingMethod? method, + bool isRawPathBaseline, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + Method = method; + IsRawPathBaseline = isRawPathBaseline; + Status = status; + Metrics = metrics ?? new PathQualityMetrics(); + Timing = timing ?? new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, "No timing result was supplied."); + StableGeometryDigest = stableGeometryDigest ?? string.Empty; + Diagnostic = diagnostic ?? string.Empty; + Path = CopyReadOnly(path); + Segments = CopyReadOnly(segments); + } + + public SmoothingMethod? Method { get; } + + public bool IsRawPathBaseline { get; } + + public PathSmoothingStatus Status { get; } + + public PathQualityMetrics Metrics { get; } + + public SmoothingTimingSummary Timing { get; } + + public string StableGeometryDigest { get; } + + public string Diagnostic { get; } + + public IReadOnlyList Path { get; } + + public IReadOnlyList Segments { get; } + + public bool IsEligibleForRecommendation => + !IsRawPathBaseline && + IsPublishedLocalG2Status(Status) && + Metrics.IsFeasible && + Timing.IsDeterministic; + + internal static bool IsPublishedLocalG2Status(PathSmoothingStatus status) + { + return status == PathSmoothingStatus.Complete || + status == PathSmoothingStatus.PartialImprovement || + status == PathSmoothingStatus.NotNeeded || + status == PathSmoothingStatus.Unchanged; + } + + internal static PathSmoothingComparisonEntry CreateCandidate( + SmoothingMethod method, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + return new PathSmoothingComparisonEntry( + method, false, status, metrics, timing, stableGeometryDigest, diagnostic, path, segments); + } + + internal static PathSmoothingComparisonEntry CreateRawPathBaseline( + PathSmoothingStatus status, + PathQualityMetrics metrics, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + return new PathSmoothingComparisonEntry( + null, true, status, metrics, + new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, true, string.Empty), + stableGeometryDigest, diagnostic, path, segments); + } + + private static IReadOnlyList Empty() + { + return new ReadOnlyCollection(new List()); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonRequest.cs new file mode 100644 index 0000000..968227b --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonRequest.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +/// Immutable request for comparing a coarse-path baseline with Local G2 output. +public sealed class PathSmoothingComparisonRequest +{ + private static readonly IReadOnlyList LocalG2OnlyMethods = + new ReadOnlyCollection(new[] { SmoothingMethod.LocalG2Quintic }); + + public PathSmoothingComparisonRequest(PathSmoothingRequest smoothingRequest) + { + SmoothingRequest = CopyRequest(smoothingRequest); + Methods = LocalG2OnlyMethods; + } + + public PathSmoothingRequest SmoothingRequest { get; } + + /// The report always contains the sole supported method, Local G2 quintic. + public IReadOnlyList Methods { get; } + + private static PathSmoothingRequest CopyRequest(PathSmoothingRequest source) + { + if (source == null) return null; + return new PathSmoothingRequest( + source.CoarsePath, + source.Segments, + source.Map, + source.Vehicle, + source.Configuration); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonResult.cs new file mode 100644 index 0000000..59fb4c7 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/PathSmoothingComparisonResult.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +/// Immutable raw-path and Local G2 comparison result. +public sealed class PathSmoothingComparisonResult +{ + internal PathSmoothingComparisonResult( + PathSmoothingComparisonEntry rawPathBaseline, + IReadOnlyList entries, + SmoothingMethod? recommendedMethod, + bool isCancelled, + string diagnostic) + { + RawPathBaseline = rawPathBaseline ?? throw new ArgumentNullException(nameof(rawPathBaseline)); + Entries = CopyReadOnly(entries); + RecommendedMethod = recommendedMethod; + IsCancelled = isCancelled; + Diagnostic = diagnostic ?? string.Empty; + } + + public PathSmoothingComparisonEntry RawPathBaseline { get; } + + public IReadOnlyList Entries { get; } + + /// Local G2 when its published result is deterministic; otherwise null. + public SmoothingMethod? RecommendedMethod { get; } + + public bool IsCancelled { get; } + + public string Diagnostic { get; } + + private static IReadOnlyList CopyReadOnly( + IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + } + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingTimingSummary.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingTimingSummary.cs new file mode 100644 index 0000000..f29c64c --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingTimingSummary.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +/// 一个方法的固定五次计时样本和确定性结论。 +public sealed class SmoothingTimingSummary +{ + /// 创建一个只接受五个已测量样本的计时汇总。 + public SmoothingTimingSummary( + IReadOnlyList measuredElapsedMilliseconds, + bool isDeterministic, + string diagnostic) + { + if (measuredElapsedMilliseconds == null || measuredElapsedMilliseconds.Count != 5) + throw new ArgumentException("计时汇总必须包含五个已测量样本。", nameof(measuredElapsedMilliseconds)); + + var copy = new List(measuredElapsedMilliseconds.Count); + for (int index = 0; index < measuredElapsedMilliseconds.Count; index++) + { + double value = measuredElapsedMilliseconds[index]; + if (double.IsNaN(value) || double.IsInfinity(value) || value < 0d) + throw new ArgumentOutOfRangeException(nameof(measuredElapsedMilliseconds), "计时样本必须为有限非负数。"); + copy.Add(value); + } + + MeasuredElapsedMilliseconds = new ReadOnlyCollection(copy); + MedianElapsedMilliseconds = Median(copy); + IsDeterministic = isDeterministic; + Diagnostic = diagnostic ?? string.Empty; + } + + /// 不含预热执行的五个实测耗时,单位 ms。 + public IReadOnlyList MeasuredElapsedMilliseconds { get; } + + /// 五个实测耗时的稳定中位数,单位 ms。 + public double MedianElapsedMilliseconds { get; } + + /// 五次输出是否具有相同的状态和稳定几何摘要。 + public bool IsDeterministic { get; } + + /// 非确定性或测量失败的稳定诊断说明。 + public string Diagnostic { get; } + + /// 从五次测量结果中生成计时汇总,并拒绝状态或几何不稳定的输出。 + public static SmoothingTimingSummary FromMeasurements( + IReadOnlyList measuredElapsedMilliseconds, + IReadOnlyList measuredResults) + { + if (measuredResults == null || measuredResults.Count != 5) + throw new ArgumentException("确定性检查必须包含五个测量结果。", nameof(measuredResults)); + for (int index = 0; index < measuredResults.Count; index++) + { + if (measuredResults[index] == null) + throw new ArgumentException("确定性检查不能包含空测量结果。", nameof(measuredResults)); + } + + PathSmoothingResult canonical = measuredResults[0]; + string canonicalDigest = StableGeometryDigest.Compute(canonical); + for (int index = 1; index < measuredResults.Count; index++) + { + PathSmoothingResult measured = measuredResults[index]; + string digest = StableGeometryDigest.Compute(measured); + if (measured.Status != canonical.Status || + measured.Path.Count != canonical.Path.Count || + measured.Segments.Count != canonical.Segments.Count || + !string.Equals(digest, canonicalDigest, StringComparison.Ordinal)) + { + return new SmoothingTimingSummary( + measuredElapsedMilliseconds, + false, + "五次测量的状态、点数、分段数或稳定几何摘要不一致。"); + } + } + + return new SmoothingTimingSummary(measuredElapsedMilliseconds, true, string.Empty); + } + + internal static double Median(IReadOnlyList values) + { + var sorted = new double[values.Count]; + for (int index = 0; index < values.Count; index++) sorted[index] = values[index]; + Array.Sort(sorted); + int middle = sorted.Length / 2; + return sorted.Length % 2 == 1 + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2d; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/StableGeometryDigest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/StableGeometryDigest.cs new file mode 100644 index 0000000..8940f44 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/StableGeometryDigest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +/// 为重复执行结果生成与进程无关的稳定几何 SHA-256 摘要。 +public static class StableGeometryDigest +{ + /// 计算正式平滑结果的状态、方法、分段和路径位模式摘要。 + public static string Compute(PathSmoothingResult result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + return Compute(result.Status, result.Method, result.Path, result.Segments); + } + + /// 计算任意已分析路径的稳定摘要。 + public static string Compute( + PathSmoothingStatus status, + SmoothingMethod? method, + IReadOnlyList path, + IReadOnlyList segments) + { + using (var stream = new MemoryStream()) + { + WriteInt32(stream, 1); + WriteInt32(stream, (int)status); + WriteBoolean(stream, method.HasValue); + if (method.HasValue) WriteInt32(stream, (int)method.Value); + + WriteInt32(stream, path == null ? 0 : path.Count); + if (path != null) + { + for (int index = 0; index < path.Count; index++) WritePoint(stream, path[index]); + } + + WriteInt32(stream, segments == null ? 0 : segments.Count); + if (segments != null) + { + for (int index = 0; index < segments.Count; index++) WriteSegment(stream, segments[index]); + } + + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(stream.ToArray()); + var builder = new StringBuilder(hash.Length * 2); + for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2")); + return builder.ToString(); + } + } + } + + private static void WritePoint(Stream stream, SmoothedPathPoint point) + { + if (point == null) throw new ArgumentException("稳定路径摘要不能包含空点。", nameof(point)); + WriteDouble(stream, point.X); + WriteDouble(stream, point.Y); + WriteDouble(stream, point.Heading); + WriteDouble(stream, point.UnwrappedHeading); + WriteDouble(stream, point.ArcLength); + WriteInt32(stream, (int)point.Direction); + WriteDouble(stream, point.GeometricCurvature); + WriteDouble(stream, point.VehicleCurvature); + WriteDouble(stream, point.BodyClearance); + WriteBoolean(stream, point.IsGearSwitchPoint); + WriteInt32(stream, (int)point.Source); + } + + private static void WriteSegment(Stream stream, SmoothedPathSegment segment) + { + if (segment == null) throw new ArgumentException("稳定路径摘要不能包含空方向段。", nameof(segment)); + WriteInt32(stream, segment.SegmentIndex); + WriteInt32(stream, (int)segment.Direction); + WriteInt32(stream, segment.StartIndex); + WriteInt32(stream, segment.EndIndex); + WriteBoolean(stream, segment.StartsAtGearSwitch); + WriteBoolean(stream, segment.EndsAtGearSwitch); + } + + private static void WriteDouble(Stream stream, double value) + { + WriteInt64(stream, BitConverter.DoubleToInt64Bits(value)); + } + + private static void WriteBoolean(Stream stream, bool value) + { + stream.WriteByte(value ? (byte)1 : (byte)0); + } + + private static void WriteInt32(Stream stream, int value) + { + unchecked + { + stream.WriteByte((byte)value); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 24)); + } + } + + private static void WriteInt64(Stream stream, long value) + { + unchecked + { + ulong bits = (ulong)value; + for (int index = 0; index < 8; index++) stream.WriteByte((byte)(bits >> (index * 8))); + } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/IeeeFigureStyle.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/IeeeFigureStyle.cs new file mode 100644 index 0000000..b33f1ca --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/IeeeFigureStyle.cs @@ -0,0 +1,12 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// Fixed dimensions and colors for Local G2 comparison reports. +public static class IeeeFigureStyle +{ + public const double FigureWidthPoints = 7.16d * 72d; + public const double FigureHeightPoints = 5.20d * 72d; + public const string RawColor = "#4D4D4D"; + public const string LocalG2Color = "#0072B2"; + public const string LocalG2DiagnosticColor = "#B1373E"; + public const string LimitColor = "#CC79A7"; +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingCsvWriter.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingCsvWriter.cs new file mode 100644 index 0000000..c32e7d3 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingCsvWriter.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 把共享图形模型的指标表写为带 BOM 的 UTF-8 CSV。 +public sealed class SmoothingCsvWriter +{ + private const string Header = "ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic"; + + public byte[] Write(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + var text = new StringBuilder(Header).Append("\r\n"); + for (int index = 0; index < model.MetricRows.Count; index++) + { + SmoothingFigureMetricRow row = model.MetricRows[index]; + PathQualityMetrics metrics = row.Metrics; + text.Append(Field(model.ScenarioId)).Append(',').Append(Field(row.Method)).Append(',').Append(Field(row.Status.ToString())).Append(',') + .Append(Number(metrics.PathLengthMeters)).Append(',').Append(Number(metrics.MaximumAbsoluteVehicleCurvaturePerMeter)).Append(',') + .Append(Number(metrics.RootMeanSquareVehicleCurvaturePerMeter)).Append(',').Append(Number(metrics.TotalAbsoluteCurvatureVariationPerMeter)).Append(',') + .Append(Number(metrics.CurvatureVariationEnergy)).Append(',').Append(Number(metrics.MinimumBodyClearanceMeters)).Append(',') + .Append(Number(row.Timing == null ? 0d : row.Timing.MedianElapsedMilliseconds)).Append(',') + .Append(row.Timing == null ? 0 : row.Timing.MeasuredElapsedMilliseconds.Count).Append(',') + .Append(row.Timing != null && row.Timing.IsDeterministic ? "true" : "false").Append("\r\n"); + } + byte[] body = new UTF8Encoding(false).GetBytes(text.ToString()); + byte[] preamble = new UTF8Encoding(true).GetPreamble(); + var output = new byte[preamble.Length + body.Length]; + Buffer.BlockCopy(preamble, 0, output, 0, preamble.Length); + Buffer.BlockCopy(body, 0, output, preamble.Length, body.Length); + return output; + } + + private static string Number(double value) { return value.ToString("0.#################", CultureInfo.InvariantCulture); } + private static string Field(string value) + { + string text = value ?? string.Empty; + return text.IndexOfAny(new[] { ',', '\"', '\r', '\n' }) < 0 ? text : "\"" + text.Replace("\"", "\"\"") + "\""; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureDefinition.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureDefinition.cs new file mode 100644 index 0000000..6536345 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureDefinition.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 单张 SVG/PNG 共享的不可变绘图视图。 +public sealed class SmoothingFigureDefinition +{ + internal SmoothingFigureDefinition( + SmoothingFigureKind kind, + string fileStem, + string title, + SmoothingFigureModel model, + bool showsMapContext, + IReadOnlyList series, + double worldXMinMeters, + double worldXMaxMeters, + double worldYMinMeters, + double worldYMaxMeters, + IReadOnlyList xTicks, + IReadOnlyList yTicks, + double curvatureArcLengthMaximumMeters, + double curvatureMinimumPerMeter, + double curvatureMaximumPerMeter, + IReadOnlyList curvatureArcLengthTicks, + IReadOnlyList curvatureTicks) + { + Kind = kind; + FileStem = fileStem ?? string.Empty; + Title = title ?? string.Empty; + Model = model ?? throw new ArgumentNullException(nameof(model)); + ShowsMapContext = showsMapContext; + Series = Copy(series); + WorldXMinMeters = worldXMinMeters; + WorldXMaxMeters = worldXMaxMeters; + WorldYMinMeters = worldYMinMeters; + WorldYMaxMeters = worldYMaxMeters; + XTicks = Copy(xTicks); + YTicks = Copy(yTicks); + CurvatureArcLengthMaximumMeters = curvatureArcLengthMaximumMeters; + CurvatureMinimumPerMeter = curvatureMinimumPerMeter; + CurvatureMaximumPerMeter = curvatureMaximumPerMeter; + CurvatureArcLengthTicks = Copy(curvatureArcLengthTicks); + CurvatureTicks = Copy(curvatureTicks); + } + + public SmoothingFigureKind Kind { get; } + public string FileStem { get; } + public string Title { get; } + public SmoothingFigureModel Model { get; } + public bool ShowsMapContext { get; } + public bool IsCurvatureFigure => Kind == SmoothingFigureKind.CurvatureComparison; + public double FigureWidthPoints => Model.FigureWidthPoints; + public double FigureHeightPoints => Model.FigureHeightPoints; + public double PlotXPoints => 68d; + public double PlotYPoints => 44d; + public double PlotWidthPoints => 400d; + public double PlotHeightPoints => 245d; + public double LegendYPoints => LegendEntries.Count > 4 ? 332d : 340d; + public IReadOnlyList Series { get; } + public IReadOnlyList LegendEntries => BuildLegend(Series); + public double WorldXMinMeters { get; } + public double WorldXMaxMeters { get; } + public double WorldYMinMeters { get; } + public double WorldYMaxMeters { get; } + public double WorldScalePointsPerMeter => Math.Min(PlotWidthPoints / (WorldXMaxMeters - WorldXMinMeters), PlotHeightPoints / (WorldYMaxMeters - WorldYMinMeters)); + public IReadOnlyList XTicks { get; } + public IReadOnlyList YTicks { get; } + public double CurvatureArcLengthMaximumMeters { get; } + public double CurvatureMinimumPerMeter { get; } + public double CurvatureMaximumPerMeter { get; } + public IReadOnlyList CurvatureArcLengthTicks { get; } + public IReadOnlyList CurvatureTicks { get; } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList BuildLegend(IReadOnlyList views) + { + var entries = new List(views == null ? 0 : views.Count); + if (views != null) + { + for (int index = 0; index < views.Count; index++) + { + SmoothingFigureSeries series = views[index].Series; + entries.Add(new SmoothingFigureLegendEntry(series.Label + " (" + series.Status + ")", series.Color, string.Empty)); + } + } + return new ReadOnlyCollection(entries); + } +} + +/// 某条路径在特定图中的显示透明度。 +public sealed class SmoothingFigureSeriesView +{ + internal SmoothingFigureSeriesView(SmoothingFigureSeries series, double opacity, double pointRadiusPoints = 1.35d) + { + Series = series ?? throw new ArgumentNullException(nameof(series)); + Opacity = opacity < 0d ? 0d : (opacity > 1d ? 1d : opacity); + PointRadiusPoints = pointRadiusPoints > 0d ? pointRadiusPoints : 1.35d; + } + + public SmoothingFigureSeries Series { get; } + public double Opacity { get; } + public double PointRadiusPoints { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureKind.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureKind.cs new file mode 100644 index 0000000..f32ecdb --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureKind.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// Stable report figures for the raw-path and Local G2 workflow. +public enum SmoothingFigureKind +{ + CoarsePathOverview, + AllPathsComparison, + LocalG2Overview, + CurvatureComparison, + LocalG2DiagnosticCandidate, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModel.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModel.cs new file mode 100644 index 0000000..2e6bddc --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModel.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// SVG 和 PNG 共享的不可变路径平滑报告图形模型。 +public sealed class SmoothingFigureModel +{ + internal SmoothingFigureModel( + string scenarioId, + string scenarioLabel, + double worldXMinMeters, + double worldXMaxMeters, + double worldYMinMeters, + double worldYMaxMeters, + double pathPanelX, + double pathPanelY, + double pathPanelWidth, + double pathPanelHeight, + double curvaturePanelX, + double curvaturePanelY, + double curvaturePanelWidth, + double curvaturePanelHeight, + double metricsPanelX, + double metricsPanelY, + double metricsPanelWidth, + double metricsPanelHeight, + IReadOnlyList obstacles, + IReadOnlyList series, + IReadOnlyList metricRows, + SmoothingFigurePoint start, + SmoothingFigurePoint goal) + { + ScenarioId = scenarioId ?? string.Empty; + ScenarioLabel = scenarioLabel ?? string.Empty; + WorldXMinMeters = worldXMinMeters; + WorldXMaxMeters = worldXMaxMeters; + WorldYMinMeters = worldYMinMeters; + WorldYMaxMeters = worldYMaxMeters; + PathPanelX = pathPanelX; + PathPanelY = pathPanelY; + PathPanelWidth = pathPanelWidth; + PathPanelHeight = pathPanelHeight; + CurvaturePanelX = curvaturePanelX; + CurvaturePanelY = curvaturePanelY; + CurvaturePanelWidth = curvaturePanelWidth; + CurvaturePanelHeight = curvaturePanelHeight; + MetricsPanelX = metricsPanelX; + MetricsPanelY = metricsPanelY; + MetricsPanelWidth = metricsPanelWidth; + MetricsPanelHeight = metricsPanelHeight; + Obstacles = Copy(obstacles); + Series = Copy(series); + MetricRows = Copy(metricRows); + Start = start ?? throw new ArgumentNullException(nameof(start)); + Goal = goal ?? throw new ArgumentNullException(nameof(goal)); + } + + public string ScenarioId { get; } + public string ScenarioLabel { get; } + public double FigureWidthPoints => IeeeFigureStyle.FigureWidthPoints; + public double FigureHeightPoints => IeeeFigureStyle.FigureHeightPoints; + public string PathPanelLabel => "(a)"; + public string CurvaturePanelLabel => "(b)"; + public string MetricsPanelLabel => "(c)"; + public double WorldXMinMeters { get; } + public double WorldXMaxMeters { get; } + public double WorldYMinMeters { get; } + public double WorldYMaxMeters { get; } + public double PathPanelX { get; } + public double PathPanelY { get; } + public double PathPanelWidth { get; } + public double PathPanelHeight { get; } + public double CurvaturePanelX { get; } + public double CurvaturePanelY { get; } + public double CurvaturePanelWidth { get; } + public double CurvaturePanelHeight { get; } + public double MetricsPanelX { get; } + public double MetricsPanelY { get; } + public double MetricsPanelWidth { get; } + public double MetricsPanelHeight { get; } + public double PathScaleX { get; internal set; } + public double PathScaleY { get; internal set; } + public IReadOnlyList Obstacles { get; } + public IReadOnlyList Series { get; } + public IReadOnlyList LegendEntries => BuildLegend(Series); + public IReadOnlyList MetricRows { get; } + public SmoothingFigurePoint Start { get; } + public SmoothingFigurePoint Goal { get; } + + internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series) + { + if (series == null) throw new ArgumentNullException(nameof(series)); + var combined = new List(Series.Count + 1); + for (int index = 0; index < Series.Count; index++) + { + if (Series[index].Key == series.Key) + throw new ArgumentException("Figure series keys must be unique.", nameof(series)); + combined.Add(Series[index]); + } + combined.Add(series); + var copy = new SmoothingFigureModel( + ScenarioId, ScenarioLabel, WorldXMinMeters, WorldXMaxMeters, WorldYMinMeters, WorldYMaxMeters, + PathPanelX, PathPanelY, PathPanelWidth, PathPanelHeight, + CurvaturePanelX, CurvaturePanelY, CurvaturePanelWidth, CurvaturePanelHeight, + MetricsPanelX, MetricsPanelY, MetricsPanelWidth, MetricsPanelHeight, + Obstacles, combined, MetricRows, Start, Goal) + { + PathScaleX = PathScaleX, + PathScaleY = PathScaleY, + }; + return copy; + } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } + + private static IReadOnlyList BuildLegend(IReadOnlyList series) + { + var legend = new List(series == null ? 0 : series.Count); + if (series != null) + { + for (int index = 0; index < series.Count; index++) + legend.Add(new SmoothingFigureLegendEntry(series[index].Label, series[index].Color, series[index].DashArray)); + } + return new ReadOnlyCollection(legend); + } +} + +public sealed class SmoothingFigurePoint +{ + public SmoothingFigurePoint(double xMeters, double yMeters, double arcLengthMeters, double vehicleCurvaturePerMeter) + { + X = xMeters; Y = yMeters; ArcLength = arcLengthMeters; VehicleCurvature = vehicleCurvaturePerMeter; + } + public double X { get; } + public double Y { get; } + public double ArcLength { get; } + public double VehicleCurvature { get; } +} + +public sealed class SmoothingFigureObstacle +{ + public SmoothingFigureObstacle(double xMeters, double yMeters, double widthMeters, double heightMeters) + { + X = xMeters; Y = yMeters; Width = widthMeters; Height = heightMeters; + } + public double X { get; } + public double Y { get; } + public double Width { get; } + public double Height { get; } +} + +public sealed class SmoothingFigureSeries +{ + internal SmoothingFigureSeries(SmoothingMethod? method, string key, string label, PathSmoothingStatus status, string color, string dashArray, + bool isRawPathBaseline, IReadOnlyList points, IReadOnlyList violationMarkers) + { + Method = method; Key = key ?? string.Empty; Label = label ?? string.Empty; Status = status; Color = color ?? string.Empty; + DashArray = dashArray ?? string.Empty; IsRawPathBaseline = isRawPathBaseline; Points = Copy(points); ViolationMarkers = Copy(violationMarkers); + } + public SmoothingMethod? Method { get; } + public string Key { get; } + public string Label { get; } + public PathSmoothingStatus Status { get; } + public string Color { get; } + public string DashArray { get; } + public bool IsRawPathBaseline { get; } + public bool IsCurveVisible => Points.Count > 0; + public IReadOnlyList Points { get; } + public IReadOnlyList ViolationMarkers { get; } + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} + +public sealed class SmoothingFigureLegendEntry +{ + internal SmoothingFigureLegendEntry(string label, string color, string dashArray) { Label = label ?? string.Empty; Color = color ?? string.Empty; DashArray = dashArray ?? string.Empty; } + public string Label { get; } + public string Color { get; } + public string DashArray { get; } +} + +public sealed class SmoothingFigureMetricRow +{ + internal SmoothingFigureMetricRow(string method, string label, PathSmoothingStatus status, PathQualityMetrics metrics, + SmoothingTimingSummary timing) + { + Method = method ?? string.Empty; Label = label ?? string.Empty; Status = status; Metrics = metrics ?? new PathQualityMetrics(); + Timing = timing; + } + public string Method { get; } + public string Label { get; } + public PathSmoothingStatus Status { get; } + public PathQualityMetrics Metrics { get; } + public SmoothingTimingSummary Timing { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModelBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModelBuilder.cs new file mode 100644 index 0000000..95b35f2 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureModelBuilder.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// Transforms the raw-path and Local G2 results into shared report-figure data. +public sealed class SmoothingFigureModelBuilder +{ + private const double MarginPoints = 18d; + private const double PanelGapPoints = 12d; + + public SmoothingFigureModel Build( + PathSmoothingComparisonResult comparison, + PlanningGridMap map, + Pose2D start, + Pose2D goal, + string scenarioId, + string scenarioLabel) + { + if (comparison == null) throw new ArgumentNullException(nameof(comparison)); + if (map == null) throw new ArgumentNullException(nameof(map)); + if (start == null) throw new ArgumentNullException(nameof(start)); + if (goal == null) throw new ArgumentNullException(nameof(goal)); + + double worldXMin = map.Bounds.XMin / 1000d; + double worldXMax = map.Bounds.XMax / 1000d; + double worldYMin = map.Bounds.YMin / 1000d; + double worldYMax = map.Bounds.YMax / 1000d; + double innerWidth = IeeeFigureStyle.FigureWidthPoints - 2d * MarginPoints; + double innerHeight = IeeeFigureStyle.FigureHeightPoints - 2d * MarginPoints; + double pathWidth = innerWidth * 0.60d; + double rightWidth = innerWidth - pathWidth - PanelGapPoints; + double rightHeight = (innerHeight - PanelGapPoints) / 2d; + + var model = new SmoothingFigureModel( + scenarioId, scenarioLabel, worldXMin, worldXMax, worldYMin, worldYMax, + MarginPoints, MarginPoints, pathWidth, innerHeight, + MarginPoints + pathWidth + PanelGapPoints, MarginPoints, rightWidth, rightHeight, + MarginPoints + pathWidth + PanelGapPoints, MarginPoints + rightHeight + PanelGapPoints, rightWidth, rightHeight, + BuildObstacles(map), BuildSeries(comparison, map), BuildMetricRows(comparison), + new SmoothingFigurePoint(start.X, start.Y, 0d, 0d), new SmoothingFigurePoint(goal.X, goal.Y, 0d, 0d)); + + double worldWidth = worldXMax - worldXMin; + double worldHeight = worldYMax - worldYMin; + if (worldWidth <= 0d || worldHeight <= 0d) + throw new ArgumentOutOfRangeException(nameof(map), "Map bounds must be finite and non-degenerate."); + double scale = Math.Min(pathWidth / worldWidth, innerHeight / worldHeight); + model.PathScaleX = scale; + model.PathScaleY = scale; + return model; + } + + private static IReadOnlyList BuildObstacles(PlanningGridMap map) + { + var runs = new List(); + double resolution = map.ResolutionMeters; + double xMin = map.Bounds.XMin / 1000d; + double yMin = map.Bounds.YMin / 1000d; + for (int row = 0; row < map.Rows; row++) + { + int runStart = -1; + for (int column = 0; column <= map.Cols; column++) + { + bool occupied = column < map.Cols && map.IsOccupied(row, column); + if (occupied && runStart < 0) { runStart = column; continue; } + if (!occupied && runStart >= 0) + { + AddOrExtendRun(runs, xMin + runStart * resolution, yMin + row * resolution, + (column - runStart) * resolution, resolution); + runStart = -1; + } + } + } + var obstacles = new List(runs.Count); + for (int index = 0; index < runs.Count; index++) + { + ObstacleRun run = runs[index]; + obstacles.Add(new SmoothingFigureObstacle(run.X, run.Y, run.Width, run.Height)); + } + return obstacles; + } + + private static void AddOrExtendRun(IList runs, double x, double y, double width, double height) + { + for (int index = runs.Count - 1; index >= 0; index--) + { + ObstacleRun candidate = runs[index]; + if (NearlyEqual(candidate.X, x) && NearlyEqual(candidate.Width, width) && NearlyEqual(candidate.Y + candidate.Height, y)) + { + candidate.Height += height; + return; + } + } + runs.Add(new ObstacleRun(x, y, width, height)); + } + + private static bool NearlyEqual(double first, double second) => Math.Abs(first - second) < 1e-9d; + + private static IReadOnlyList BuildSeries(PathSmoothingComparisonResult comparison, PlanningGridMap map) + { + return new List(2) + { + CreateSeries(comparison.RawPathBaseline, null, "raw", "Raw coarse path", IeeeFigureStyle.RawColor, string.Empty, true, map), + CreateSeries(FindLocalG2(comparison), SmoothingMethod.LocalG2Quintic, "local-g2", "Local G2", IeeeFigureStyle.LocalG2Color, string.Empty, false, map), + }; + } + + private static IReadOnlyList BuildMetricRows(PathSmoothingComparisonResult comparison) + { + return new List + { + CreateRow(comparison.RawPathBaseline, "RawPath", "Raw coarse path"), + CreateRow(FindLocalG2(comparison), "LocalG2Quintic", "Local G2"), + }; + } + + private static PathSmoothingComparisonEntry FindLocalG2(PathSmoothingComparisonResult comparison) + { + for (int index = 0; index < comparison.Entries.Count; index++) + { + PathSmoothingComparisonEntry entry = comparison.Entries[index]; + if (entry.Method == SmoothingMethod.LocalG2Quintic) return entry; + } + return null; + } + + private static SmoothingFigureMetricRow CreateRow(PathSmoothingComparisonEntry entry, string method, string label) + { + return entry == null + ? new SmoothingFigureMetricRow(method, label, PathSmoothingStatus.Failed, new PathQualityMetrics(), null) + : new SmoothingFigureMetricRow(method, label, entry.Status, entry.Metrics, entry.Timing); + } + + private static SmoothingFigureSeries CreateSeries( + PathSmoothingComparisonEntry entry, + SmoothingMethod? method, + string key, + string label, + string color, + string dashArray, + bool isRawPathBaseline, + PlanningGridMap map) + { + var points = new List(); + var violations = new List(); + PathSmoothingStatus status = entry == null ? PathSmoothingStatus.Failed : entry.Status; + if (entry != null) + { + for (int index = 0; index < entry.Path.Count; index++) + { + SmoothedPathPoint point = entry.Path[index]; + var figurePoint = new SmoothingFigurePoint(point.X, point.Y, point.ArcLength, point.VehicleCurvature); + points.Add(figurePoint); + if (status == PathSmoothingStatus.Infeasible && map.IsOccupiedWorld(point.X, point.Y)) violations.Add(figurePoint); + } + } + if (status == PathSmoothingStatus.Infeasible && points.Count > 0 && violations.Count == 0) + violations.Add(points[points.Count / 2]); + return new SmoothingFigureSeries(method, key, label, status, color, dashArray, isRawPathBaseline, points, violations); + } + + private sealed class ObstacleRun + { + public ObstacleRun(double x, double y, double width, double height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public double X { get; } + public double Y { get; } + public double Width { get; } + public double Height { get; set; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureSetBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureSetBuilder.cs new file mode 100644 index 0000000..f5e49c2 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFigureSetBuilder.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// Builds the stable four-figure Local G2 report set and an optional diagnostic figure. +public sealed class SmoothingFigureSetBuilder +{ + private const double MinimumExtentMeters = 0.25d; + private const double PaddingFraction = 0.10d; + + public SmoothingFigureSet Build(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + SmoothingFigureSeries raw = Find(model, "raw"); + SmoothingFigureSeries localG2 = Find(model, "local-g2"); + var figures = new List + { + BuildOverhead(SmoothingFigureKind.CoarsePathOverview, "01-coarse-path-overview", "Raw coarse path", model, true, View(raw, 1d)), + BuildOverhead(SmoothingFigureKind.AllPathsComparison, "02-all-paths-comparison", "Raw path and Local G2", model, false, View(raw, 1d), View(localG2, 1d)), + BuildOverhead(SmoothingFigureKind.LocalG2Overview, "03-local-g2-overview", "Local G2 smoothing", model, true, View(raw, 0.28d), View(localG2, 1d)), + BuildCurvature(model, View(raw, 1d), View(localG2, 1d)), + }; + if (TryFind(model, "local-g2-diagnostic", out SmoothingFigureSeries diagnostic)) + { + figures.Add(BuildOverhead( + SmoothingFigureKind.LocalG2DiagnosticCandidate, + "05-local-g2-diagnostic-candidate", + "Local G2 diagnostic candidate (not published)", + model, + true, + View(raw, 1d, 2.10d), + View(diagnostic, 1d))); + } + return new SmoothingFigureSet(figures); + } + + private static SmoothingFigureDefinition BuildOverhead( + SmoothingFigureKind kind, + string stem, + string title, + SmoothingFigureModel model, + bool mapContext, + params SmoothingFigureSeriesView[] series) + { + Bounds bounds = CalculateWorldBounds(model, mapContext, series); + return new SmoothingFigureDefinition( + kind, stem, title, model, mapContext, series, + bounds.XMin, bounds.XMax, bounds.YMin, bounds.YMax, + BuildTicks(bounds.XMin, bounds.XMax), BuildTicks(bounds.YMin, bounds.YMax), + 1d, -1d, 1d, Array.Empty(), Array.Empty()); + } + + private static SmoothingFigureDefinition BuildCurvature( + SmoothingFigureModel model, + params SmoothingFigureSeriesView[] series) + { + double arcMaximum = 0d; + double curvatureMinimum = 0d; + double curvatureMaximum = 0d; + for (int viewIndex = 0; viewIndex < series.Length; viewIndex++) + { + IReadOnlyList points = series[viewIndex].Series.Points; + for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) + { + SmoothingFigurePoint point = points[pointIndex]; + if (point.ArcLength > arcMaximum) arcMaximum = point.ArcLength; + if (point.VehicleCurvature < curvatureMinimum) curvatureMinimum = point.VehicleCurvature; + if (point.VehicleCurvature > curvatureMaximum) curvatureMaximum = point.VehicleCurvature; + } + } + arcMaximum = ExpandMaximum(arcMaximum, MinimumExtentMeters); + ExpandRange(ref curvatureMinimum, ref curvatureMaximum, MinimumExtentMeters); + return new SmoothingFigureDefinition( + SmoothingFigureKind.CurvatureComparison, + "04-curvature-comparison", + "Vehicle-curvature comparison", + model, + false, + series, + 0d, + 1d, + 0d, + 1d, + Array.Empty(), + Array.Empty(), + arcMaximum, + curvatureMinimum, + curvatureMaximum, + BuildTicks(0d, arcMaximum), + BuildTicks(curvatureMinimum, curvatureMaximum)); + } + + private static Bounds CalculateWorldBounds( + SmoothingFigureModel model, + bool includesEndpoints, + IReadOnlyList series) + { + bool hasPoint = false; + double xMin = 0d; + double xMax = 0d; + double yMin = 0d; + double yMax = 0d; + for (int viewIndex = 0; viewIndex < series.Count; viewIndex++) + { + IReadOnlyList points = series[viewIndex].Series.Points; + for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) + Include(points[pointIndex].X, points[pointIndex].Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + } + if (includesEndpoints) + { + Include(model.Start.X, model.Start.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + Include(model.Goal.X, model.Goal.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax); + } + if (!hasPoint) + { + xMin = model.WorldXMinMeters; + xMax = model.WorldXMaxMeters; + yMin = model.WorldYMinMeters; + yMax = model.WorldYMaxMeters; + } + ExpandRange(ref xMin, ref xMax, MinimumExtentMeters); + ExpandRange(ref yMin, ref yMax, MinimumExtentMeters); + double xPadding = (xMax - xMin) * PaddingFraction; + double yPadding = (yMax - yMin) * PaddingFraction; + xMin -= xPadding; + xMax += xPadding; + yMin -= yPadding; + yMax += yPadding; + const double desiredAspect = 400d / 245d; + double width = xMax - xMin; + double height = yMax - yMin; + if (width / height < desiredAspect) + { + double halfWidth = height * desiredAspect / 2d; + double center = (xMin + xMax) / 2d; + xMin = center - halfWidth; + xMax = center + halfWidth; + } + else + { + double halfHeight = width / desiredAspect / 2d; + double center = (yMin + yMax) / 2d; + yMin = center - halfHeight; + yMax = center + halfHeight; + } + return new Bounds(xMin, xMax, yMin, yMax); + } + + private static void Include(double x, double y, ref bool hasPoint, ref double xMin, ref double xMax, ref double yMin, ref double yMax) + { + if (!hasPoint) + { + hasPoint = true; + xMin = x; + xMax = x; + yMin = y; + yMax = y; + return; + } + if (x < xMin) xMin = x; + if (x > xMax) xMax = x; + if (y < yMin) yMin = y; + if (y > yMax) yMax = y; + } + + private static void ExpandRange(ref double minimum, ref double maximum, double minimumExtent) + { + double extent = maximum - minimum; + if (extent >= minimumExtent) return; + double center = (minimum + maximum) / 2d; + minimum = center - minimumExtent / 2d; + maximum = center + minimumExtent / 2d; + } + + private static double ExpandMaximum(double value, double minimum) + { + return value < minimum ? minimum : value * (1d + PaddingFraction); + } + + private static IReadOnlyList BuildTicks(double minimum, double maximum) + { + double span = maximum - minimum; + if (span <= 0d) return new[] { minimum, maximum }; + double roughStep = span / 5d; + double magnitude = Math.Pow(10d, Math.Floor(Math.Log10(roughStep))); + double normalized = roughStep / magnitude; + double nice = normalized <= 1d ? 1d : (normalized <= 2d ? 2d : (normalized <= 5d ? 5d : 10d)); + double step = nice * magnitude; + var ticks = new List(); + double first = Math.Ceiling(minimum / step) * step; + for (double value = first; value <= maximum + step * 0.001d; value += step) ticks.Add(value); + if (ticks.Count < 2) + { + ticks.Clear(); + ticks.Add(minimum); + ticks.Add(maximum); + } + return new ReadOnlyCollection(ticks); + } + + private static SmoothingFigureSeries Find(SmoothingFigureModel model, string key) + { + for (int index = 0; index < model.Series.Count; index++) + { + if (model.Series[index].Key == key) return model.Series[index]; + } + throw new InvalidOperationException("Figure model is missing path series: " + key); + } + + private static bool TryFind(SmoothingFigureModel model, string key, out SmoothingFigureSeries series) + { + for (int index = 0; index < model.Series.Count; index++) + { + if (model.Series[index].Key == key) + { + series = model.Series[index]; + return true; + } + } + series = null; + return false; + } + + private static SmoothingFigureSeriesView View( + SmoothingFigureSeries series, + double opacity, + double pointRadiusPoints = 1.35d) + { + return new SmoothingFigureSeriesView(series, opacity, pointRadiusPoints); + } + + private readonly struct Bounds + { + public Bounds(double xMin, double xMax, double yMin, double yMax) + { + XMin = xMin; + XMax = xMax; + YMin = yMin; + YMax = yMax; + } + + public double XMin { get; } + public double XMax { get; } + public double YMin { get; } + public double YMax { get; } + } +} + +/// Stable normal figure order; an optional Local G2 diagnostic figure is appended. +public sealed class SmoothingFigureSet +{ + internal SmoothingFigureSet(IReadOnlyList figures) + { + var copy = new List(figures == null ? 0 : figures.Count); + if (figures != null) + { + for (int index = 0; index < figures.Count; index++) copy.Add(figures[index]); + } + Figures = new ReadOnlyCollection(copy); + } + + public IReadOnlyList Figures { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFontResolver.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFontResolver.cs new file mode 100644 index 0000000..2780242 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingFontResolver.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 按准确族名解析报告所需字体,并提供混合中英文文本的共同基线度量。 +public sealed class SmoothingFontResolver +{ + public bool TryResolve( + string chineseFamilyName, + string latinFamilyName, + out SmoothingFontResolution resolution, + out string reason) + { + resolution = null; + reason = string.Empty; + if (string.IsNullOrWhiteSpace(chineseFamilyName) || string.IsNullOrWhiteSpace(latinFamilyName)) + { + reason = "中文和拉丁字体族名均不能为空。"; + return false; + } + + var collection = new InstalledFontCollection(); + FontFamily chinese = FindExact(collection.Families, chineseFamilyName); + FontFamily latin = FindExact(collection.Families, latinFamilyName); + if (chinese == null || latin == null) + { + collection.Dispose(); + reason = "未安装报告所需的精确字体族:" + (chinese == null ? chineseFamilyName : latinFamilyName) + "。"; + return false; + } + + resolution = new SmoothingFontResolution(collection, chinese, latin, chineseFamilyName, latinFamilyName); + return true; + } + + public RectangleF MeasureMixedText(SmoothingFontResolution resolution, string text, float points) + { + if (resolution == null) throw new ArgumentNullException(nameof(resolution)); + if (string.IsNullOrEmpty(text) || points <= 0f) return RectangleF.Empty; + using (var bitmap = new Bitmap(1, 1)) + using (Graphics graphics = Graphics.FromImage(bitmap)) + using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) + { + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + float width = 0f, height = 0f; + foreach (TextRun run in SplitRuns(text)) + { + using (Font font = resolution.CreateFont(run.IsChinese ? resolution.ChineseFamily : resolution.LatinFamily, points)) + { + SizeF size = graphics.MeasureString(run.Text, font, PointF.Empty, format); + width += size.Width; + height = Math.Max(height, size.Height); + } + } + return new RectangleF(0f, 0f, width, height); + } + } + + internal static IReadOnlyList SplitRuns(string text) + { + var runs = new List(); + if (string.IsNullOrEmpty(text)) return runs; + int start = 0; + bool isChinese = IsChinese(text[0]); + for (int index = 1; index < text.Length; index++) + { + bool currentIsChinese = IsChinese(text[index]); + if (currentIsChinese == isChinese) continue; + runs.Add(new TextRun(text.Substring(start, index - start), isChinese)); + start = index; + isChinese = currentIsChinese; + } + runs.Add(new TextRun(text.Substring(start), isChinese)); + return runs; + } + + private static FontFamily FindExact(IReadOnlyList families, string name) + { + for (int index = 0; index < families.Count; index++) + { + FontFamily family = families[index]; + if (string.Equals(family.Name, name, StringComparison.Ordinal) || + string.Equals(family.GetName(1033), name, StringComparison.Ordinal)) return family; + } + return null; + } + + private static bool IsChinese(char value) + { + return (value >= 0x3400 && value <= 0x4dbf) || (value >= 0x4e00 && value <= 0x9fff) || + (value >= 0xf900 && value <= 0xfaff) || value == 0x3002 || value == 0xff0c || value == 0xff1a; + } + + internal sealed class TextRun + { + public TextRun(string text, bool isChinese) { Text = text; IsChinese = isChinese; } + public string Text { get; } + public bool IsChinese { get; } + } +} + +/// 一次报告导出所使用的精确字体族;释放时同时释放字体集合。 +public sealed class SmoothingFontResolution : IDisposable +{ + private readonly InstalledFontCollection _collection; + private readonly string _chineseFamilyName; + private readonly string _latinFamilyName; + + internal SmoothingFontResolution( + InstalledFontCollection collection, + FontFamily chineseFamily, + FontFamily latinFamily, + string chineseFamilyName, + string latinFamilyName) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + ChineseFamily = chineseFamily ?? throw new ArgumentNullException(nameof(chineseFamily)); + LatinFamily = latinFamily ?? throw new ArgumentNullException(nameof(latinFamily)); + _chineseFamilyName = chineseFamilyName ?? throw new ArgumentNullException(nameof(chineseFamilyName)); + _latinFamilyName = latinFamilyName ?? throw new ArgumentNullException(nameof(latinFamilyName)); + } + + public string ChineseFamilyName => _chineseFamilyName; + public string LatinFamilyName => _latinFamilyName; + internal FontFamily ChineseFamily { get; } + internal FontFamily LatinFamily { get; } + + internal Font CreateFont(FontFamily family, float points) + { + return new Font(family, points, FontStyle.Regular, GraphicsUnit.Point); + } + + public void Dispose() + { + _collection.Dispose(); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingPngRenderer.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingPngRenderer.cs new file mode 100644 index 0000000..67c596d --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingPngRenderer.cs @@ -0,0 +1,287 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 仅 Windows 运行时使用 GDI+ 将每张共享图形定义渲染为 600 dpi PNG;轨迹仅绘制离散点。 +public sealed class SmoothingPngRenderer +{ + public const int WidthPixels = 4296; + public const int HeightPixels = 3120; + public const uint PixelsPerMeter = 23622u; + private const float PixelsPerPoint = 600f / 72f; + + public byte[] Render(SmoothingFigureModel model, SmoothingFontResolution fonts) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1], fonts); + } + + public byte[] Render(SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + if (fonts == null) throw new ArgumentNullException(nameof(fonts)); + using (var bitmap = new Bitmap(WidthPixels, HeightPixels, PixelFormat.Format32bppArgb)) + { + bitmap.SetResolution(600f, 600f); + using (Graphics graphics = Graphics.FromImage(bitmap)) + { + graphics.Clear(Color.White); + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + DrawFigure(graphics, figure, fonts); + } + return Encode(bitmap); + } + } + + private static void DrawFigure(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawMixedText(graphics, fonts, figure.Title + " — " + figure.Model.ScenarioLabel, PointX(figure.PlotXPoints), PointY(12d), 12f, Color.Black); + if (figure.IsCurvatureFigure) DrawCurvatureAxes(graphics, figure, fonts); else DrawOverheadAxes(graphics, figure, fonts); + GraphicsState state = graphics.Save(); + graphics.SetClip(new RectangleF(PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints))); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawObstacles(graphics, figure); + if (figure.IsCurvatureFigure) DrawCurvaturePoints(graphics, figure); else DrawPathPoints(graphics, figure); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawStartGoal(graphics, figure); + graphics.Restore(state); + DrawLegend(graphics, figure, fonts); + } + + private static void DrawOverheadAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawPlotFrame(graphics, figure); + using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) + { + for (int index = 0; index < figure.XTicks.Count; index++) + { + float x = WorldX(figure, figure.XTicks[index]); + graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); + DrawMixedText(graphics, fonts, Number(figure.XTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); + } + for (int index = 0; index < figure.YTicks.Count; index++) + { + float y = WorldY(figure, figure.YTicks[index]); + graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + DrawMixedText(graphics, fonts, Number(figure.YTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); + } + } + DrawMixedText(graphics, fonts, "X (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 11d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); + DrawVerticalText(graphics, fonts, "Y (m)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 17d), 10f, Color.Black); + } + + private static void DrawCurvatureAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + DrawPlotFrame(graphics, figure); + using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint)) + { + for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) + { + float x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); + graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints)); + DrawMixedText(graphics, fonts, Number(figure.CurvatureArcLengthTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black); + } + for (int index = 0; index < figure.CurvatureTicks.Count; index++) + { + float y = CurvatureY(figure, figure.CurvatureTicks[index]); + graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + DrawMixedText(graphics, fonts, Number(figure.CurvatureTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black); + } + } + if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) + { + float y = CurvatureY(figure, 0d); + using (var zero = new Pen(Color.FromArgb(77, 77, 77), 0.65f * PixelsPerPoint)) + graphics.DrawLine(zero, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y); + } + DrawMixedText(graphics, fonts, "s (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 10d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black); + DrawVerticalText(graphics, fonts, "κ (m⁻¹)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 22d), 10f, Color.Black); + } + + private static void DrawPlotFrame(Graphics graphics, SmoothingFigureDefinition figure) + { + using (var border = new Pen(Color.Black, 0.75f * PixelsPerPoint)) + graphics.DrawRectangle(border, PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints)); + } + + private static void DrawObstacles(Graphics graphics, SmoothingFigureDefinition figure) + { + using (var fill = new SolidBrush(Color.FromArgb(217, 217, 217))) + using (var outline = new Pen(Color.FromArgb(128, 128, 128), 0.35f * PixelsPerPoint)) + { + for (int index = 0; index < figure.Model.Obstacles.Count; index++) + { + SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; + float x = WorldX(figure, obstacle.X); + float y = WorldY(figure, obstacle.Y + obstacle.Height); + float width = (float)(obstacle.Width * figure.WorldScalePointsPerMeter * PixelsPerPoint); + float height = (float)(obstacle.Height * figure.WorldScalePointsPerMeter * PixelsPerPoint); + graphics.FillRectangle(fill, x, y, width, height); + graphics.DrawRectangle(outline, x, y, width, height); + } + } + } + + private static void DrawPathPoints(Graphics graphics, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + DrawPoints(graphics, view, point => new PointF(WorldX(figure, point.X), WorldY(figure, point.Y))); + DrawViolations(graphics, figure, view); + } + } + + private static void DrawCurvaturePoints(Graphics graphics, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + DrawPoints(graphics, view, point => new PointF(CurvatureX(figure, point.ArcLength), CurvatureY(figure, point.VehicleCurvature))); + } + } + + private static void DrawPoints(Graphics graphics, SmoothingFigureSeriesView view, Func transform) + { + if (!view.Series.IsCurveVisible) return; + using (var fill = new SolidBrush(WithOpacity(ColorFromHex(view.Series.Color), view.Opacity))) + { + float radius = (float)(view.PointRadiusPoints * PixelsPerPoint); + for (int index = 0; index < view.Series.Points.Count; index++) + { + PointF point = transform(view.Series.Points[index]); + graphics.FillEllipse(fill, point.X - radius, point.Y - radius, radius * 2f, radius * 2f); + } + } + } + + private static void DrawViolations(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFigureSeriesView view) + { + if (view.Series.ViolationMarkers.Count == 0) return; + using (var marker = new Pen(ColorFromHex(view.Series.Color), 1.1f * PixelsPerPoint)) + { + float radius = 3f * PixelsPerPoint; + for (int index = 0; index < view.Series.ViolationMarkers.Count; index++) + { + SmoothingFigurePoint point = view.Series.ViolationMarkers[index]; + float x = WorldX(figure, point.X), y = WorldY(figure, point.Y); + graphics.DrawLine(marker, x - radius, y - radius, x + radius, y + radius); + graphics.DrawLine(marker, x - radius, y + radius, x + radius, y - radius); + } + } + } + + private static void DrawStartGoal(Graphics graphics, SmoothingFigureDefinition figure) + { + float startX = WorldX(figure, figure.Model.Start.X), startY = WorldY(figure, figure.Model.Start.Y), radius = 3.2f * PixelsPerPoint; + using (var startBrush = new SolidBrush(Color.FromArgb(240, 228, 66))) + using (var border = new Pen(Color.Black, 0.8f * PixelsPerPoint)) + using (var goalBrush = new SolidBrush(ColorFromHex(IeeeFigureStyle.LimitColor))) + { + graphics.FillEllipse(startBrush, startX - radius, startY - radius, radius * 2f, radius * 2f); + graphics.DrawEllipse(border, startX - radius, startY - radius, radius * 2f, radius * 2f); + float goalX = WorldX(figure, figure.Model.Goal.X), goalY = WorldY(figure, figure.Model.Goal.Y), diamond = 4f * PixelsPerPoint; + PointF[] points = { new PointF(goalX, goalY - diamond), new PointF(goalX + diamond, goalY), new PointF(goalX, goalY + diamond), new PointF(goalX - diamond, goalY) }; + graphics.FillPolygon(goalBrush, points); + graphics.DrawPolygon(border, points); + } + } + + private static void DrawLegend(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts) + { + const double columnWidth = 198d; + for (int index = 0; index < figure.LegendEntries.Count; index++) + { + SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; + int column = index % 2, row = index / 2; + float x = PointX(figure.PlotXPoints + column * columnWidth); + float y = PointY(figure.LegendYPoints + row * 15d - 3d); + using (var fill = new SolidBrush(ColorFromHex(entry.Color))) graphics.FillEllipse(fill, x, y - 2.2f * PixelsPerPoint, 4.4f * PixelsPerPoint, 4.4f * PixelsPerPoint); + DrawMixedText(graphics, fonts, entry.Label, x + 10f * PixelsPerPoint, y - 5f * PixelsPerPoint, 8.5f, Color.Black); + } + } + + private static void DrawVerticalText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float y, float points, Color color) + { + GraphicsState state = graphics.Save(); + graphics.TranslateTransform(x, y); + graphics.RotateTransform(-90f); + DrawMixedText(graphics, fonts, text, 0f, 0f, points, color); + graphics.Restore(state); + } + + private static void DrawMixedText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float top, float points, Color color) + { + var runs = SmoothingFontResolver.SplitRuns(text); + float emPixels = points * PixelsPerPoint, maxAscent = 0f; + for (int index = 0; index < runs.Count; index++) + { + FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; + maxAscent = Math.Max(maxAscent, family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular)); + } + float baseline = top + maxAscent; + using (var brush = new SolidBrush(color)) + using (var format = (StringFormat)StringFormat.GenericTypographic.Clone()) + { + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + for (int index = 0; index < runs.Count; index++) + { + FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily; + using (Font font = fonts.CreateFont(family, points)) + { + float runTop = baseline - family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular); + graphics.DrawString(runs[index].Text, font, brush, x, runTop, format); + x += graphics.MeasureString(runs[index].Text, font, PointF.Empty, format).Width; + } + } + } + } + + private static byte[] Encode(Bitmap bitmap) + { + Rectangle rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + BitmapData data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + int sourceLength = checked(data.Stride * bitmap.Height); + var source = new byte[sourceLength]; + Marshal.Copy(data.Scan0, source, 0, source.Length); + var rgba = new byte[checked(bitmap.Width * bitmap.Height * 4)]; + for (int y = 0; y < bitmap.Height; y++) + { + int sourceRow = y * data.Stride, outputRow = y * bitmap.Width * 4; + for (int x = 0; x < bitmap.Width; x++) + { + int sourceOffset = sourceRow + x * 4, outputOffset = outputRow + x * 4; + rgba[outputOffset] = source[sourceOffset + 2]; + rgba[outputOffset + 1] = source[sourceOffset + 1]; + rgba[outputOffset + 2] = source[sourceOffset]; + rgba[outputOffset + 3] = source[sourceOffset + 3]; + } + } + using (var output = new MemoryStream()) + { + ValidatedPngWriter.Write(rgba, bitmap.Width, bitmap.Height, output, PixelsPerMeter); + return output.ToArray(); + } + } + finally { bitmap.UnlockBits(data); } + } + + private static Color ColorFromHex(string value) { return ColorTranslator.FromHtml(value); } + private static Color WithOpacity(Color color, double opacity) { return Color.FromArgb((int)Math.Round(255d * opacity), color.R, color.G, color.B); } + private static float PointX(double points) { return (float)(points * PixelsPerPoint); } + private static float PointY(double points) { return (float)(points * PixelsPerPoint); } + private static float WorldX(SmoothingFigureDefinition figure, double x) { return PointX(figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter); } + private static float WorldY(SmoothingFigureDefinition figure, double y) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter); } + private static float CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return PointX(figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints); } + private static float CurvatureY(SmoothingFigureDefinition figure, double curvature) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints); } + private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportRequest.cs new file mode 100644 index 0000000..6efd21e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportRequest.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 一次报告导出的共享图形模型、目标目录与精确字体要求。 +public sealed class SmoothingReportExportRequest +{ + public SmoothingFigureModel Model { get; set; } + public string OutputDirectory { get; set; } + public string FileStem { get; set; } + public string ChineseFontFamilyName { get; set; } = "SimSun"; + public string LatinFontFamilyName { get; set; } = "Times New Roman"; +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportResult.cs new file mode 100644 index 0000000..fd7ba44 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExportResult.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 报告导出结果的稳定状态。 +public enum SmoothingReportExportStatus +{ + Success, + InvalidInput, + FontUnavailable, + Failed, +} + +/// 六张常规 SVG、六张常规 PNG 与一个 CSV 的发布结果;增强模型携带诊断序列时追加可选 07-local-g2-diagnostic-candidate,失败时不返回部分输出路径。 +public sealed class SmoothingReportExportResult +{ + internal SmoothingReportExportResult(SmoothingReportExportStatus status, string reason, IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) + { + Status = status; + Reason = reason ?? string.Empty; + SvgPaths = Copy(svgPaths); + PngPaths = Copy(pngPaths); + CsvPath = csvPath ?? string.Empty; + } + + public SmoothingReportExportStatus Status { get; } + public string Reason { get; } + public IReadOnlyList SvgPaths { get; } + public IReadOnlyList PngPaths { get; } + public string CsvPath { get; } + + internal static SmoothingReportExportResult Success(IReadOnlyList svgPaths, IReadOnlyList pngPaths, string csvPath) + { + return new SmoothingReportExportResult(SmoothingReportExportStatus.Success, string.Empty, svgPaths, pngPaths, csvPath); + } + + internal static SmoothingReportExportResult Failure(SmoothingReportExportStatus status, string reason) + { + return new SmoothingReportExportResult(status, reason, new string[0], new string[0], string.Empty); + } + + private static IReadOnlyList Copy(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) for (int index = 0; index < source.Count; index++) copy.Add(source[index] ?? string.Empty); + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExporter.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExporter.cs new file mode 100644 index 0000000..262f992 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingReportExporter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 以同级临时文件生成六张常规 SVG、六张常规 PNG 和 CSV 后成组发布;增强模型携带诊断序列时追加可选 07-local-g2-diagnostic-candidate。 +public sealed class SmoothingReportExporter +{ + private readonly SmoothingFontResolver _fontResolver = new SmoothingFontResolver(); + private readonly SmoothingFigureSetBuilder _figureSetBuilder = new SmoothingFigureSetBuilder(); + private readonly SmoothingSvgRenderer _svgRenderer = new SmoothingSvgRenderer(); + private readonly SmoothingPngRenderer _pngRenderer = new SmoothingPngRenderer(); + private readonly SmoothingCsvWriter _csvWriter = new SmoothingCsvWriter(); + + public SmoothingReportExportResult Export(SmoothingReportExportRequest request) + { + if (request == null || request.Model == null || string.IsNullOrWhiteSpace(request.OutputDirectory) || !IsFileStem(request.FileStem)) + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.InvalidInput, "报告模型、输出目录或文件名无效。"); + if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution availableFonts, out string fontReason)) + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.FontUnavailable, fontReason); + availableFonts.Dispose(); + + var pending = new List(); + var svgPaths = new List(); + var pngPaths = new List(); + string csvPath = Path.Combine(request.OutputDirectory, request.FileStem + ".csv"); + try + { + SmoothingFigureSet figures = _figureSetBuilder.Build(request.Model); + for (int index = 0; index < figures.Figures.Count; index++) + { + SmoothingFigureDefinition figure = figures.Figures[index]; + string svgPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".svg"); + string pngPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".png"); + svgPaths.Add(svgPath); + pngPaths.Add(pngPath); + pending.Add(new PendingFile(svgPath, Encoding.UTF8.GetBytes(_svgRenderer.Render(figure)))); + if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution renderFonts, out fontReason)) + throw new InvalidOperationException(fontReason); + byte[] png; + using (renderFonts) png = _pngRenderer.Render(figure, renderFonts); + pending.Add(new PendingFile(pngPath, png)); + } + pending.Add(new PendingFile(csvPath, _csvWriter.Write(request.Model))); + Directory.CreateDirectory(request.OutputDirectory); + for (int index = 0; index < pending.Count; index++) File.WriteAllBytes(pending[index].TemporaryPath, pending[index].Content); + PublishAll(pending); + DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.svg")); + DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.png")); + return SmoothingReportExportResult.Success(svgPaths, pngPaths, csvPath); + } + catch (Exception exception) + { + RestorePublishedFiles(pending); + return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.Failed, exception.Message); + } + finally + { + for (int index = 0; index < pending.Count; index++) + { + DeleteIfExists(pending[index].TemporaryPath); + DeleteIfExists(pending[index].BackupPath); + } + } + } + + private static void PublishAll(IReadOnlyList files) + { + string transaction = Guid.NewGuid().ToString("N"); + for (int index = 0; index < files.Count; index++) + { + PendingFile file = files[index]; + file.ExistedBeforePublish = File.Exists(file.FinalPath); + file.BackupPath = file.ExistedBeforePublish ? file.FinalPath + ".backup-" + transaction : string.Empty; + if (file.ExistedBeforePublish) File.Replace(file.TemporaryPath, file.FinalPath, file.BackupPath); + else File.Move(file.TemporaryPath, file.FinalPath); + file.Published = true; + } + for (int index = 0; index < files.Count; index++) DeleteIfExists(files[index].BackupPath); + } + + private static void RestorePublishedFiles(IReadOnlyList files) + { + for (int index = files.Count - 1; index >= 0; index--) + { + PendingFile file = files[index]; + if (!file.Published) continue; + try + { + if (file.ExistedBeforePublish && File.Exists(file.BackupPath)) + { + if (File.Exists(file.FinalPath)) File.Replace(file.BackupPath, file.FinalPath, null); + else File.Move(file.BackupPath, file.FinalPath); + } + else if (!file.ExistedBeforePublish) DeleteIfExists(file.FinalPath); + } + catch { } + } + } + + private static bool IsFileStem(string value) + { + return !string.IsNullOrWhiteSpace(value) && value.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 && value.IndexOf(Path.DirectorySeparatorChar) < 0 && value.IndexOf(Path.AltDirectorySeparatorChar) < 0; + } + + private static void DeleteIfExists(string path) + { + if (!string.IsNullOrEmpty(path) && File.Exists(path)) File.Delete(path); + } + + private sealed class PendingFile + { + public PendingFile(string finalPath, byte[] content) + { + FinalPath = finalPath; + Content = content; + TemporaryPath = finalPath + ".tmp"; + BackupPath = string.Empty; + } + + public string FinalPath { get; } + public byte[] Content { get; } + public string TemporaryPath { get; } + public string BackupPath { get; set; } + public bool ExistedBeforePublish { get; set; } + public bool Published { get; set; } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingSvgRenderer.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingSvgRenderer.cs new file mode 100644 index 0000000..49c04e1 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingSvgRenderer.cs @@ -0,0 +1,189 @@ +using System; +using System.Globalization; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +/// 把单张共享图形定义渲染为 UTF-8 XML 可编辑 SVG;轨迹仅由离散点组成。 +public sealed class SmoothingSvgRenderer +{ + public string Render(SmoothingFigureModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1]); + } + + public string Render(SmoothingFigureDefinition figure) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + var svg = new StringBuilder(); + svg.Append("\n\n") + .Append("\n") + .Append("\n"); + AppendTitle(svg, figure); + if (figure.IsCurvatureFigure) AppendCurvatureAxes(svg, figure); else AppendOverheadAxes(svg, figure); + svg.Append("\n"); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendObstacles(svg, figure); + if (figure.IsCurvatureFigure) AppendCurvaturePoints(svg, figure); else AppendPathPoints(svg, figure); + if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendStartGoal(svg, figure); + svg.Append("\n"); + AppendLegend(svg, figure); + svg.Append(""); + return svg.ToString(); + } + + private static void AppendTitle(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("") + .Append(Escape(figure.Title)).Append(" — ").Append(Escape(figure.Model.ScenarioLabel)).Append("\n"); + } + + private static void AppendOverheadAxes(StringBuilder svg, SmoothingFigureDefinition figure) + { + AppendPlotFrame(svg, figure); + for (int index = 0; index < figure.XTicks.Count; index++) + { + double x = WorldX(figure, figure.XTicks[index]); + AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); + svg.Append("").Append(Number(figure.XTicks[index])).Append("\n"); + } + for (int index = 0; index < figure.YTicks.Count; index++) + { + double y = WorldY(figure, figure.YTicks[index]); + AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); + svg.Append("").Append(Number(figure.YTicks[index])).Append("\n"); + } + svg.Append("X (m)\n") + .Append("Y (m)\n"); + } + + private static void AppendCurvatureAxes(StringBuilder svg, SmoothingFigureDefinition figure) + { + AppendPlotFrame(svg, figure); + for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) + { + double x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); + AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); + svg.Append("").Append(Number(figure.CurvatureArcLengthTicks[index])).Append("\n"); + } + for (int index = 0; index < figure.CurvatureTicks.Count; index++) + { + double y = CurvatureY(figure, figure.CurvatureTicks[index]); + AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); + svg.Append("").Append(Number(figure.CurvatureTicks[index])).Append("\n"); + } + if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) + { + double zero = CurvatureY(figure, 0d); + svg.Append("\n"); + } + svg.Append("s (m)\n") + .Append("κ (m⁻¹)\n"); + } + + private static void AppendPlotFrame(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("\n"); + } + + private static void AppendGridLine(StringBuilder svg, double x1, double y1, double x2, double y2) + { + svg.Append("\n"); + } + + private static void AppendObstacles(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int index = 0; index < figure.Model.Obstacles.Count; index++) + { + SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; + svg.Append("\n"); + } + } + + private static void AppendPathPoints(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + AppendPoints(svg, view, point => WorldX(figure, point.X), point => WorldY(figure, point.Y)); + for (int markerIndex = 0; markerIndex < view.Series.ViolationMarkers.Count; markerIndex++) AppendCross(svg, figure, view.Series.ViolationMarkers[markerIndex], view.Series.Color); + } + } + + private static void AppendCurvaturePoints(StringBuilder svg, SmoothingFigureDefinition figure) + { + for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) + { + SmoothingFigureSeriesView view = figure.Series[viewIndex]; + AppendPoints(svg, view, point => CurvatureX(figure, point.ArcLength), point => CurvatureY(figure, point.VehicleCurvature)); + } + } + + private static void AppendPoints(StringBuilder svg, SmoothingFigureSeriesView view, Func x, Func y) + { + if (!view.Series.IsCurveVisible) return; + svg.Append("\n"); + for (int index = 0; index < view.Series.Points.Count; index++) + { + SmoothingFigurePoint point = view.Series.Points[index]; + svg.Append("\n"); + } + svg.Append("\n"); + } + + private static void AppendCross(StringBuilder svg, SmoothingFigureDefinition figure, SmoothingFigurePoint point, string color) + { + double x = WorldX(figure, point.X), y = WorldY(figure, point.Y), radius = 3d; + svg.Append("\n"); + } + + private static void AppendStartGoal(StringBuilder svg, SmoothingFigureDefinition figure) + { + svg.Append("\n"); + double x = WorldX(figure, figure.Model.Goal.X), y = WorldY(figure, figure.Model.Goal.Y), radius = 4d; + svg.Append("\n"); + } + + private static void AppendLegend(StringBuilder svg, SmoothingFigureDefinition figure) + { + double columnWidth = 198d; + for (int index = 0; index < figure.LegendEntries.Count; index++) + { + SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; + int column = index % 2; + int row = index / 2; + double x = figure.PlotXPoints + column * columnWidth; + double y = figure.LegendYPoints + row * 15d; + svg.Append("\n") + .Append(Escape(entry.Label)).Append("\n"); + } + } + + private static double WorldX(SmoothingFigureDefinition figure, double x) { return figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter; } + private static double WorldY(SmoothingFigureDefinition figure, double y) { return figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter; } + private static double CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints; } + private static double CurvatureY(SmoothingFigureDefinition figure, double curvature) { return figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints; } + private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } + private static string Escape(string value) { return (value ?? string.Empty).Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs index b9d34f8..cbde98d 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs @@ -37,6 +37,7 @@ internal static class RawPathBaselineBuilder request.Map, request.Vehicle, maximumCollisionCheckStepMeters, + request.Configuration.CurvatureLimitRadiusToleranceMeters, out IReadOnlyList safePath, out double minimumClearanceMeters, out reason)) @@ -55,6 +56,7 @@ internal static class RawPathBaselineBuilder request.Map, request.Vehicle, maximumCollisionCheckStepMeters, + request.Configuration.CurvatureLimitRadiusToleranceMeters, out safePath, out minimumClearanceMeters, out reason)) @@ -146,7 +148,7 @@ internal static class RawPathBaselineBuilder double geometricCurvature = directionSign * point.VehicleCurvature; SmoothedPathPointSource source = point.IsGearSwitchPoint ? SmoothedPathPointSource.GearSwitch - : SmoothedPathPointSource.CoarsePathFallback; + : SmoothedPathPointSource.Anchor; path.Add(new SmoothedPathPoint( point.X, point.Y, diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md new file mode 100644 index 0000000..2b2ccb8 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md @@ -0,0 +1,137 @@ +# PathSmoothing 路径平滑(Local G2) + +## 模块说明(Module Overview) + +`PathSmoothing` 是 Hybrid A* 成功输出后的空间参考路径后处理模块。它只保留 Local G2 五次过渡算法:在同一行驶方向的粗路径段内检测车辆曲率跳变,尝试以局部五次曲线替换,并对整条结果进行车辆曲率、全车体碰撞、净空和曲率变化代价复核。 + +该模块不参与粗路径搜索接受条件,也不做时间参数化、速度、加速度、转向速率或动态障碍物处理。下游只能消费本模块已经发布的空间路径。 + +## 文件结构(File Structure) + +```text +PathSmoothing/ +├── Contracts/ # 请求、结果、配置、状态、质量指标与区域报告 +├── Facade/ # 正式平滑入口和离线比较入口 +├── LocalG2/ # 跳变检测、窗口规划、候选生成、评估、拼接 +├── Processing/ # 粗路径预处理、原始基线和几何分析 +├── Validation/ # 全车体碰撞与车辆曲率复核 +├── Output/ +│ ├── Comparison/ # 原始路径与 Local G2 的稳定比较模型和摘要 +│ └── Visualization/ # SVG、PNG、CSV 报告模型和导出器 +├── Test/ # 固定夹具、演示入口和 Local G2 诊断证据 +│ └── Fixtures/ # 版本化的成功粗路径快照 +└── README.md +``` + +`Algorithms/`、旧的 `Comparison/`、旧的 `Visualization/` 以及 B-spline、局部 Bezier、分段五次选项均已移除。输出相关代码统一归入 `Output/`,与 `CoarsePath/Output/` 的组织方式保持一致。 + +## 平滑数据流(Smoothing Data Flow) + +```text +成功的 CoarsePath + -> PathSmoothingRequest 快照 + -> 输入校验与 PathSmoothingPreprocessor + -> RawPathBaselineBuilder 原始路径全局复核 + -> CurvatureTransitionDetector(仅同向 PathSegment 内) + -> LocalG2WindowPlanner + -> LocalG2CandidateBuilder / LocalG2CandidateEvaluator + -> LocalG2PathSplicer + -> 全路径曲率、碰撞、净空、代价复核 + -> PathSmoothingResult.PublishLocalG2 +``` + +Local G2 不跨换向段建立窗口。它处理的是同一前进或倒退段内、由运动原语摆角变化等原因形成的车辆曲率跳变,而不是把换向点作为平滑对象。 + +## 结果状态与发布规则(Result Status and Publication Rules) + +下列四个状态都会发布已复核的 `Path` 和 `Segments`,可由下游消费: + +| 状态 | 含义 | +| --- | --- | +| `Complete` | 检测到的所有 Local G2 区域都被接受并替换。 | +| `PartialImprovement` | 至少一个区域被替换,其余区域保留原始几何。 | +| `NotNeeded` | 没有检测到同向段内的曲率跳变;发布已复核的原始路径。 | +| `Unchanged` | 检测到区域,但所有候选都被拒绝;发布已复核的原始路径,并保留 `RegionReports`。 | + +`InvalidInput`、`Cancelled`、`Failed` 和 `Infeasible` 不发布可消费路径。`Unchanged` 不是失败,它表示“安全门或质量门没有接受新候选”,不是“原始粗路径未经验证地回退”。 + +### 区域报告与拒绝原因 + +`PathSmoothingRegionReport` 按检测顺序记录每个 Local G2 区域。区域状态为 `Improved` 或 `RetainedOriginal`;后者可进一步给出 `WindowUnavailable`、`CandidateGenerationFailed`、`Collision`、`InsufficientClearance`、`CurvatureExceeded`、`CurvatureOvershoot`、`DeviationExceeded`、`InsufficientImprovement`、`VariationCostRegression` 或 `GlobalValidationRollback`。 + +候选先进行全车体碰撞复核,再检查净空余量。`MinimumClearanceReserveMeters = 0d` 只取消额外净空储备,不会取消车辆曲率、碰撞、数值有效性、偏差或曲率变化代价门。 + +## 坐标与单位(Coordinates and Units) + +- 路径位置、弧长、窗口长度、偏差、净空和碰撞检查步长:m +- 航向:rad +- 车辆曲率:`1/m` +- 车辆曲率导数:`1/m^2` +- 地图构建输入仍沿用 mm;`PlanningGridMap` 的路径复核查询使用 m + +发布路径保留成功粗路径的首尾位姿,因此末端误差继承粗规划器已接受的目标容差。 + +## 最小调用示例(Minimal Call Example) + +```csharp +if (coarseResult.PlanningResult.Status != PlanningStatus.Success) + return; + +var request = new PathSmoothingRequest( + coarseResult.PlanningResult.Path, + coarseResult.PlanningResult.Segments, + coarseResult.MapResult.Map, + job.Vehicle, + new PathSmoothingConfiguration()); + +PathSmoothingResult result = new PathSmoothingService().Smooth(request, cancellationToken); +bool published = result.Status == PathSmoothingStatus.Complete || + result.Status == PathSmoothingStatus.PartialImprovement || + result.Status == PathSmoothingStatus.NotNeeded || + result.Status == PathSmoothingStatus.Unchanged; + +if (published) + ConsumeSpatialReference(result.Path, result.Segments); +``` + +`PathSmoothingConfiguration` 没有算法选择开关;正式入口始终执行 Local G2。 + +## 详细使用指南(Detailed Usage Guide) + +1. 长期持有 `PathSmoothingService`,只向它传入成功的粗路径、方向分段、规划地图和车辆参数。 +2. 使用 `PathSmoothingConfiguration` 调整输出采样、碰撞检查步长和 Local G2 窗口/质量门参数。 +3. `MinimumClearanceReserveMeters` 当前默认 `0d`。需要额外净空时显式设置正值;碰撞约束始终存在。 +4. 按上表判断结果是否已发布,随后消费 `Path` 和 `Segments`;不要以 `Failed` 或 `Cancelled` 的空路径继续规划。 +5. 对 `PartialImprovement` 或 `Unchanged`,读取 `RegionReports` 判断被保留的区域以及拒绝原因。 + +Local G2 关键选项位于 `LocalG2Quintic`:最小、首选和最大窗口长度,最大偏差,曲率跳变阈值,峰值曲率导数改善比,曲率变化代价回退比,以及每区候选数。 + +## 固定夹具报告与可视化(Fixture Reports and Visualization) + +`SmoothingScenarioFixtureLoader.LoadAndVerify` 会校验 JSON 夹具的版本和指纹;夹具报告不重新运行 Hybrid A*。离线比较始终输出“原始粗路径 + Local G2”两组数据。 + +`SmoothingReportExporter.Export(SmoothingReportExportRequest)` 为每个场景写入 CSV、SVG 和 PNG。常规图固定为四张: + +1. `01-coarse-path-overview` +2. `02-all-paths-comparison` +3. `03-local-g2-overview` +4. `04-curvature-comparison` + +当模型显式附带诊断候选时,额外写入 `05-local-g2-diagnostic-candidate`。诊断候选仅用于解释拒绝,不是发布路径。 + +报告默认写入 `ClumsyPilot/obj/path_smoothing_reports` 的调用方指定子目录。PNG 需要 Windows 的 `SimSun` 与 `Times New Roman`;若字体不可用,导出返回 `FontUnavailable`,SVG 和 CSV 仍可单独使用。 + +## 常见错误(Common Errors) + +- 将未成功的 CoarsePath 直接传入平滑:应先检查 `PlanningStatus.Success`。 +- 把 `Unchanged` 当作失败:它仍然包含已复核的原始路径和区域报告。 +- 认为净空余量为 0 会关闭碰撞检查:它只取消额外储备,不能绕过全车体碰撞和车辆曲率门。 +- 在换向点两侧强行连续平滑:当前窗口检测明确不跨方向段。 +- 使用过期夹具:修改粗路径场景或规划配置后,应按夹具生成/验证脚本更新快照。 + +## 第一版限制(First-Version Limits) + +- 仅平滑同向段内的车辆曲率跳变;不跨换向点。 +- 仅包含 Local G2 五次局部过渡,不提供多算法比较或算法回退。 +- 只输出几何空间路径;没有时间、速度、加速度、转向速率和动态障碍物约束。 +- Local G2 候选必须通过严格的全路径复核;在当前夹具上,`Unchanged` 和 `Failed` 仍是有价值的诊断结果,而不是自动放宽安全约束的信号。 diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json index 46aef79..cbd08f2 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json @@ -4,7 +4,7 @@ { "id": "straight", "fixtureVersion": 1, - "configurationFingerprint": "sha256:d8c625157c21789a8bd33052a7f5927e4b1f697a2567cf3644b37ce00e164c51", + "configurationFingerprint": "sha256:d42ce997e1e51d421fe923c5819a332706007d900cc74a39d4b0ac88c6ac4a21", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, @@ -1914,7 +1914,7 @@ { "id": "single-turn", "fixtureVersion": 1, - "configurationFingerprint": "sha256:a0218dbe66746b347139161d545656d6a55516f8e87769b4f9f17c5cf8d5d84f", + "configurationFingerprint": "sha256:49571e2eeb2e58f0a5dc0a56e61e530700c3da7d60e2a9c66809721963e5d9f8", "map": { "xMinMm": -7000.0, "xMaxMm": 11000.0, @@ -3020,7 +3020,7 @@ { "id": "s-bend", "fixtureVersion": 1, - "configurationFingerprint": "sha256:aa1ae45d914e3ddc181bef415d71659fb357f9e2288e3eecec52376e6ca663bc", + "configurationFingerprint": "sha256:810b601e9090644292e4eb5652379f5f0db6fa7158aaf6e4faeae4b337ad5ec5", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, @@ -6715,7 +6715,7 @@ { "id": "large-heading-change", "fixtureVersion": 1, - "configurationFingerprint": "sha256:5a5b66530a8782e81b456102602112d3e9f41f031d976e7daad035f9fa70311b", + "configurationFingerprint": "sha256:44b6c95a3b553bb04df3912d5cf7c9110f4d629e37551b2c130d8d7f22706fce", "map": { "xMinMm": -7000.0, "xMaxMm": 11000.0, @@ -8253,7 +8253,7 @@ { "id": "rectangle-detour", "fixtureVersion": 1, - "configurationFingerprint": "sha256:de5a9261b3b073e4324526ecaf0dfc9251aa4b02f85e3c07abc0c63cfe72c85c", + "configurationFingerprint": "sha256:6ad46b2782bc519b17d62dac9e238542a88f4265fb7dd3a1ebe84c04313cf6ce", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, @@ -12158,7 +12158,7 @@ { "id": "multi-obstacle-detour", "fixtureVersion": 1, - "configurationFingerprint": "sha256:278442046751c251baea0961aeb6d1bb4976498b2abb321754e5a5e941b5d2ce", + "configurationFingerprint": "sha256:d062cfcf4fec6c0119575bb0c139697d8e4d5164e958cf511c0200d0f8d9fb5d", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, @@ -15589,7 +15589,7 @@ { "id": "narrow-corridor", "fixtureVersion": 1, - "configurationFingerprint": "sha256:4ddf1423d67649faa9f52ae5ca8455c594e79ddf48374a4759a214cb5075c1b6", + "configurationFingerprint": "sha256:5ec976b32eec84abdd781b8cedeb21d07dfd6809ea61c108bcce8cff0b9d3a37", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, @@ -17520,7 +17520,7 @@ { "id": "forward-reverse-switch", "fixtureVersion": 1, - "configurationFingerprint": "sha256:68563bf572a2fb3d9db25e7639bf488e36f83c990dbf5578373e48f3ed6270ed", + "configurationFingerprint": "sha256:86e1436370027ad30a7a205a78da291f81345e6acff2c0bb4f2256803ad9d962", "map": { "xMinMm": 0.0, "xMaxMm": 6000.0, diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs new file mode 100644 index 0000000..656f21a --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +public sealed class LocalG2DiagnosticVisualizationDemo +{ + private readonly LocalG2DiagnosticEvidenceLoader _evidenceLoader = new LocalG2DiagnosticEvidenceLoader(); + private readonly PathSmoothingComparisonService _comparisonService = new PathSmoothingComparisonService(); + private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); + private readonly LocalG2PathSplicer _splicer = new LocalG2PathSplicer(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothingFigureModelBuilder _figureBuilder = new SmoothingFigureModelBuilder(); + private readonly SmoothingReportExporter _exporter = new SmoothingReportExporter(); + + public SmoothingReportExportResult Export( + string fixturePath, + string evidencePath, + string outputDirectory, + CancellationToken cancellationToken = default) + { + LocalG2DiagnosticEvidence evidence = _evidenceLoader.LoadAndVerify(evidencePath); + RequireFixtureHash(fixturePath, evidence.FixtureSha256); + PathSmoothingComparisonRequest request = FindFixtureRequest(fixturePath, evidence.ScenarioId); + PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); + PathSmoothingComparisonEntry localG2 = FindEntry(comparison, SmoothingMethod.LocalG2Quintic); + Require(localG2 != null && localG2.Status == PathSmoothingStatus.Unchanged, + "Strict Local G2 result must be Unchanged for the diagnostic evidence."); + RequireSameGeometry(comparison.RawPathBaseline.Path, localG2.Path); + Require(_preprocessor.TryPrepare(request.SmoothingRequest, out PreparedPath prepared, out string reason), reason); + LocalG2CandidateGeometry candidate = CreateCandidate(evidence); + Require(_splicer.TryReplace(prepared, candidate, out PreparedPath spliced, out reason), reason); + Require(_analyzer.TryAnalyze(spliced.Segments, request.SmoothingRequest.Configuration.OutputSpacingMeters, + out PathGeometryAnalysis analysis, out reason), reason); + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + SmoothingFigureModel normal = _figureBuilder.Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + evidence.ScenarioId, + evidence.ScenarioId); + SmoothingFigureModel augmented = normal.WithAdditionalSeries(CreateDiagnosticSeries(analysis.Path)); + return _exporter.Export(new SmoothingReportExportRequest + { + Model = augmented, + OutputDirectory = outputDirectory, + FileStem = "comparison", + }); + } + + private static void RequireFixtureHash(string fixturePath, string expectedFixtureSha256) + { + byte[] fixtureBytes = File.ReadAllBytes(fixturePath); + byte[] hash; + using (SHA256 sha256 = SHA256.Create()) + hash = sha256.ComputeHash(fixtureBytes); + string actualFixtureSha256 = BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + Require(string.Equals(actualFixtureSha256, expectedFixtureSha256, StringComparison.Ordinal), + "Diagnostic evidence fixture SHA-256 does not match the supplied fixture."); + } + + private static PathSmoothingComparisonRequest FindFixtureRequest(string fixturePath, string scenarioId) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); + Require(fixtures.Count == requests.Count, "Fixture requests do not match fixture records."); + + int fixtureIndex = -1; + for (int index = 0; index < fixtures.Count; index++) + { + if (!string.Equals(fixtures[index].Id, scenarioId, StringComparison.Ordinal)) continue; + Require(fixtureIndex < 0, "Diagnostic fixture ID must be unique: " + scenarioId); + fixtureIndex = index; + } + Require(fixtureIndex >= 0, "Diagnostic fixture ID was not found: " + scenarioId); + return requests[fixtureIndex]; + } + + private static PathSmoothingComparisonEntry FindEntry(PathSmoothingComparisonResult comparison, SmoothingMethod method) + { + if (comparison == null) return null; + for (int index = 0; index < comparison.Entries.Count; index++) + { + if (comparison.Entries[index].Method == method) return comparison.Entries[index]; + } + return null; + } + + private static LocalG2CandidateGeometry CreateCandidate(LocalG2DiagnosticEvidence evidence) + { + Require(evidence != null && evidence.CandidatePoints != null, "Diagnostic evidence is required."); + var points = new List(evidence.CandidatePoints.Count); + for (int index = 0; index < evidence.CandidatePoints.Count; index++) + { + LocalG2DiagnosticEvidencePoint point = evidence.CandidatePoints[index]; + points.Add(new SmoothingPoint2D( + point.X.Value, point.Y.Value, point.ReferenceArcLengthMeters.Value, + point.HeadingRadians.Value, point.UnwrappedHeadingRadians.Value, + 0d, false, SmoothedPathPointSource.LocalG2Transition)); + } + return new LocalG2CandidateGeometry( + evidence.CandidateIndex.Value, + evidence.SegmentIndex.Value, + evidence.WindowStartArcLengthMeters.Value, + evidence.WindowEndArcLengthMeters.Value, + 0d, + 0d, + points, + evidence.StartVehicleCurvaturePerMeter.Value, + evidence.EndVehicleCurvaturePerMeter.Value, + evidence.StartGeometricCurvaturePerMeter.Value, + evidence.EndGeometricCurvaturePerMeter.Value, + true); + } + + private static SmoothingFigureSeries CreateDiagnosticSeries(IReadOnlyList path) + { + var points = new List(path == null ? 0 : path.Count); + if (path != null) + { + for (int index = 0; index < path.Count; index++) + { + SmoothedPathPoint point = path[index]; + points.Add(new SmoothingFigurePoint(point.X, point.Y, point.ArcLength, point.VehicleCurvature)); + } + } + return new SmoothingFigureSeries( + null, + "local-g2-diagnostic", + "G2 诊断候选(净空拒绝,未发布)", + PathSmoothingStatus.Infeasible, + IeeeFigureStyle.LocalG2DiagnosticColor, + string.Empty, + false, + points, + Array.Empty()); + } + + private static void RequireSameGeometry(IReadOnlyList expected, IReadOnlyList actual) + { + Require(expected != null && actual != null && expected.Count == actual.Count, + "Strict Local G2 output must retain raw-path geometry."); + for (int index = 0; index < expected.Count; index++) + { + SmoothedPathPoint left = expected[index]; + SmoothedPathPoint right = actual[index]; + Require(left.X == right.X && left.Y == right.Y && left.Heading == right.Heading && + left.UnwrappedHeading == right.UnwrappedHeading && left.ArcLength == right.ArcLength && + left.Direction == right.Direction && left.GeometricCurvature == right.GeometricCurvature && + left.VehicleCurvature == right.VehicleCurvature && left.VehicleCurvatureDerivative == right.VehicleCurvatureDerivative && + left.BodyClearance == right.BodyClearance && left.IsGearSwitchPoint == right.IsGearSwitchPoint && left.Source == right.Source, + "Strict Local G2 output must retain raw-path geometry."); + } + } + + private static void Require(bool condition, string reason) + { + if (!condition) throw new InvalidOperationException(reason); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs new file mode 100644 index 0000000..0908d61 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +/// 开发人员离线导出八个快速夹具和四个 Hybrid A* 端到端场景的比较报告。 +public sealed class PathSmoothingComparisonDemo +{ + private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); + private readonly PathSmoothingComparisonService _comparisonService = new PathSmoothingComparisonService(); + private readonly SmoothingFigureModelBuilder _figureBuilder = new SmoothingFigureModelBuilder(); + private readonly SmoothingReportExporter _reportExporter = new SmoothingReportExporter(); + + /// 导出八个已验证夹具;不运行 Hybrid A*。 + public IReadOnlyList ExportFixtureReports( + string fixturePath, + string outputDirectory, + CancellationToken cancellationToken = default) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); + var results = new List(requests.Count); + for (int index = 0; index < requests.Count; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + results.Add(Export(fixtures[index].Id, requests[index], outputDirectory, cancellationToken)); + } + return results.AsReadOnly(); + } + + /// 运行四个固定 Hybrid A* 场景,并仅为成功粗路径导出平滑比较报告。 + public IReadOnlyList ExportEndToEndReports( + string outputDirectory, + CancellationToken cancellationToken = default) + { + CoarsePathTestScenario[] scenarios = + { + CoarsePathTestScenario.ExplicitEmpty, + CoarsePathTestScenario.RectangleDetour, + CoarsePathTestScenario.ManualAndTwoLeg, + CoarsePathTestScenario.ReverseGearSwitch, + }; + var planner = new CoarsePathPlanningService(); + var results = new List(scenarios.Length); + for (int index = 0; index < scenarios.Length; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenarios[index]); + CoarsePathPlanningJobResult coarse = planner.Plan(job, cancellationToken); + if (coarse.PlanningResult.Status != PlanningStatus.Success) + { + results.Add(PathSmoothingComparisonScenarioResult.CoarsePathFailure( + scenarios[index].ToString(), coarse.PlanningResult.Status, coarse.PlanningResult.Diagnostics.TerminationReason)); + continue; + } + results.Add(Export( + scenarios[index].ToString(), + SmoothingScenarioFactory.CreateEndToEndRequest(job, coarse), + outputDirectory, + cancellationToken)); + } + return results.AsReadOnly(); + } + + /// 供业务调用示例使用:仅在已成功的粗路径上调用平滑服务。 + public PathSmoothingResult SmoothSuccessfulCoarsePath( + CoarsePathPlanningJob job, + CoarsePathPlanningJobResult coarseResult, + PathSmoothingConfiguration configuration, + CancellationToken cancellationToken = default) + { + if (job == null || coarseResult == null || coarseResult.PlanningResult.Status != PlanningStatus.Success) + throw new ArgumentException("只有成功的粗路径能够进入平滑流程。", nameof(coarseResult)); + return _smoothingService.Smooth(new PathSmoothingRequest( + coarseResult.PlanningResult.Path, + coarseResult.PlanningResult.Segments, + coarseResult.MapResult.Map, + job.Vehicle, + configuration), cancellationToken); + } + + /// 业务调用方只可消费成功平滑或显式回退的路径。 + public static bool IsConsumable(PathSmoothingStatus status) + { + return status == PathSmoothingStatus.Complete || + status == PathSmoothingStatus.PartialImprovement || + status == PathSmoothingStatus.NotNeeded || + status == PathSmoothingStatus.Unchanged; + } + + private PathSmoothingComparisonScenarioResult Export( + string scenarioId, + PathSmoothingComparisonRequest request, + string outputDirectory, + CancellationToken cancellationToken) + { + if (request == null || request.SmoothingRequest == null || request.SmoothingRequest.CoarsePath.Count == 0) + throw new ArgumentException("比较请求必须包含粗路径。", nameof(request)); + PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + SmoothingFigureModel model = _figureBuilder.Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + scenarioId, + scenarioId); + SmoothingReportExportResult report = _reportExporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = System.IO.Path.Combine(outputDirectory, scenarioId), + FileStem = "comparison", + }); + return PathSmoothingComparisonScenarioResult.ComparisonComplete(scenarioId, comparison, report); + } +} + +/// 批处理为一个场景发布的粗路径状态、平滑比较和报告结果。 +public sealed class PathSmoothingComparisonScenarioResult +{ + private PathSmoothingComparisonScenarioResult( + string scenarioId, + PlanningStatus coarsePathStatus, + string diagnostic, + PathSmoothingComparisonResult comparison, + SmoothingReportExportResult report) + { + ScenarioId = scenarioId ?? string.Empty; + CoarsePathStatus = coarsePathStatus; + Diagnostic = diagnostic ?? string.Empty; + Comparison = comparison; + Report = report; + } + + public string ScenarioId { get; } + public PlanningStatus CoarsePathStatus { get; } + public string Diagnostic { get; } + public PathSmoothingComparisonResult Comparison { get; } + public SmoothingReportExportResult Report { get; } + + internal static PathSmoothingComparisonScenarioResult CoarsePathFailure( + string scenarioId, + PlanningStatus status, + string diagnostic) + { + return new PathSmoothingComparisonScenarioResult(scenarioId, status, diagnostic, null, null); + } + + internal static PathSmoothingComparisonScenarioResult ComparisonComplete( + string scenarioId, + PathSmoothingComparisonResult comparison, + SmoothingReportExportResult report) + { + return new PathSmoothingComparisonScenarioResult(scenarioId, PlanningStatus.Success, string.Empty, comparison, report); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs index f263573..2df6065 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; -using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs index 977e708..e66b4e9 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs @@ -239,6 +239,20 @@ public static class SmoothingScenarioFixtureLoader private static void Append(StringBuilder builder, string value) { builder.Append(value ?? string.Empty).Append('|'); } private static void Append(StringBuilder builder, int value) { builder.Append(value.ToString(CultureInfo.InvariantCulture)).Append('|'); } - private static void Append(StringBuilder builder, float value) { builder.Append(value.ToString("R", CultureInfo.InvariantCulture)).Append('|'); } - private static void Append(StringBuilder builder, double value) { builder.Append(value.ToString("R", CultureInfo.InvariantCulture)).Append('|'); } + private static void Append(StringBuilder builder, float value) + { + AppendBits(builder, BitConverter.GetBytes(value)); + } + + private static void Append(StringBuilder builder, double value) + { + AppendBits(builder, BitConverter.GetBytes(value)); + } + + private static void AppendBits(StringBuilder builder, byte[] bytes) + { + for (int index = bytes.Length - 1; index >= 0; index--) + builder.Append(bytes[index].ToString("x2", CultureInfo.InvariantCulture)); + builder.Append('|'); + } } diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs new file mode 100644 index 0000000..6e4f5c4 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/CurvatureLimitPolicy.cs @@ -0,0 +1,29 @@ +using System; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +using MultiWheelC.TrajectoryPlanning.Utils; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +internal static class CurvatureLimitPolicy +{ + internal static bool TryGetAllowedMaximumVehicleCurvaturePerMeter( + VehicleParameters vehicle, + double radiusToleranceMeters, + out double allowedMaximumCurvaturePerMeter) + { + allowedMaximumCurvaturePerMeter = 0d; + if (!NumericGuard.IsFinite(radiusToleranceMeters) || radiusToleranceMeters < 0d || + !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double nominalMaximumCurvature)) + { + return false; + } + + double nominalMinimumRadius = 1d / nominalMaximumCurvature; + double toleratedMinimumRadius = nominalMinimumRadius - radiusToleranceMeters; + if (!NumericGuard.IsPositiveFinite(toleratedMinimumRadius)) return false; + + allowedMaximumCurvaturePerMeter = 1d / toleratedMinimumRadius; + return NumericGuard.IsPositiveFinite(allowedMaximumCurvaturePerMeter); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs index f2ca415..f212e77 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs @@ -22,15 +22,23 @@ public sealed class SmoothedPathValidator } /// 创建使用指定连续车体碰撞检查器的平滑路径验证器。 + /// 以车辆几何中心位姿执行完整车体和扫掠复核的检查器,不能为 。 public SmoothedPathValidator(FootprintCollisionChecker collisionChecker) { _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); } - /// - /// 复核候选平滑路径。每个候选方向段必须保持原始段的端点和换向拓扑; - /// 输出中的净空均由本次实际车体检查重新计算,绝不沿用候选声明值。 - /// + /// 复核候选平滑路径,并以默认零转弯半径容差重新计算所有净空。 + /// 待发布候选点序列;位置/弧长单位为 m、航向为 rad、曲率为 1/m。 + /// 覆盖候选点的前进/倒车方向段集合。 + /// 预处理原始路径,候选必须保留其端点与换向拓扑。 + /// 已准备的规划地图快照。 + /// 车辆尺寸、安全余量和曲率约束。 + /// 相邻点车体扫掠最大中心步长,单位 m。 + /// 成功时为净空由真实复核重算的只读路径;失败时为空集合。 + /// 成功时为完整扩大车体的最小保守净空,单位 m;失败时为 0。 + /// 失败原因;成功时为空字符串。 + /// 端点、换向、曲率、点碰撞和扫掠均通过时为 ;否则为 ,不发布部分路径。 public bool TryValidate( IReadOnlyList candidatePath, IReadOnlyList candidateSegments, @@ -41,6 +49,35 @@ public sealed class SmoothedPathValidator out IReadOnlyList pathWithClearance, out double minimumClearanceMeters, out string reason) + { + return TryValidate( + candidatePath, candidateSegments, originalPath, map, vehicle, maximumCollisionCheckStepMeters, 0d, + out pathWithClearance, out minimumClearanceMeters, out reason); + } + + /// 以只用于曲率上限的显式转弯半径容差复核候选路径。 + /// 待复核候选点序列。 + /// 候选方向段集合。 + /// 必须保持端点和换向拓扑的预处理原始路径。 + /// 规划地图快照。 + /// 车辆几何和曲率约束。 + /// 扫掠复核最大中心步长,单位 m。 + /// 仅曲率限制使用的最小转弯半径容差,单位 m。 + /// 成功时为重新计算净空后的完整只读路径;失败时为空集合。 + /// 成功时最小保守净空,单位 m;失败时为 0。 + /// 失败诊断;成功时为空字符串。 + /// 候选满足拓扑、严格弧长、曲率、点碰撞和扫掠约束时为 ;否则为 + public bool TryValidate( + IReadOnlyList candidatePath, + IReadOnlyList candidateSegments, + PreparedPath originalPath, + PlanningGridMap map, + VehicleParameters vehicle, + double maximumCollisionCheckStepMeters, + double curvatureLimitRadiusToleranceMeters, + out IReadOnlyList pathWithClearance, + out double minimumClearanceMeters, + out string reason) { pathWithClearance = EmptyPath(); minimumClearanceMeters = 0d; @@ -52,7 +89,8 @@ public sealed class SmoothedPathValidator return false; } - if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvatureMeters)) + if (!CurvatureLimitPolicy.TryGetAllowedMaximumVehicleCurvaturePerMeter( + vehicle, curvatureLimitRadiusToleranceMeters, out double maximumCurvatureMeters)) { reason = "车辆曲率约束无效。"; return false; diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs new file mode 100644 index 0000000..5490ddc --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/AngleMath.cs @@ -0,0 +1,34 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// Deterministic angle helpers used by planning code. +public static class AngleMath +{ + public static double NormalizeRadians(double radians) + { + if (!NumericGuard.IsFinite(radians)) + return double.NaN; + + double normalized = radians % (2d * Math.PI); + if (normalized >= Math.PI) normalized -= 2d * Math.PI; + if (normalized < -Math.PI) normalized += 2d * Math.PI; + return normalized; + } + + public static double ShortestSignedDifference(double from, double to) + { + return NormalizeRadians(to - from); + } + + public static int ToHeadingIndex(double heading, double resolution, int binCount) + { + if (!NumericGuard.IsFinite(heading) || !NumericGuard.IsPositiveFinite(resolution) || binCount <= 0) + throw new ArgumentOutOfRangeException(); + + double normalized = NormalizeRadians(heading); + int index = (int)Math.Floor((normalized + Math.PI) / resolution); + index %= binCount; + return index < 0 ? index + binCount : index; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs new file mode 100644 index 0000000..44d4cea --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/CoordinateTransform.cs @@ -0,0 +1,18 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// Two-dimensional rigid-body coordinate transforms. Units are preserved. +public static class CoordinateTransform +{ + public static void LocalToWorld( + double originX, double originY, double headingRadians, + double localX, double localY, + out double worldX, out double worldY) + { + double cosine = Math.Cos(headingRadians); + double sine = Math.Sin(headingRadians); + worldX = originX + cosine * localX - sine * localY; + worldY = originY + sine * localX + cosine * localY; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs new file mode 100644 index 0000000..5683184 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/GridIndex.cs @@ -0,0 +1,15 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public readonly struct GridIndex : IEquatable +{ + public GridIndex(int row, int col) { Row = row; Col = col; } + public int Row { get; } + public int Col { get; } + public bool Equals(GridIndex other) { return Row == other.Row && Col == other.Col; } + public override bool Equals(object obj) { return obj is GridIndex && Equals((GridIndex)obj); } + public override int GetHashCode() { unchecked { return Row * 397 ^ Col; } } + public static bool operator ==(GridIndex left, GridIndex right) { return left.Equals(right); } + public static bool operator !=(GridIndex left, GridIndex right) { return !left.Equals(right); } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs new file mode 100644 index 0000000..0714ad4 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/NumericGuard.cs @@ -0,0 +1,14 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public static class NumericGuard +{ + public static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } + public static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } + public static bool IsPositiveFinite(double value) { return IsFinite(value) && value > 0d; } + public static bool IsInRange(float value, float minimum, float maximum) + { + return IsFinite(value) && value >= minimum && value <= maximum; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs new file mode 100644 index 0000000..2217a45 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +/// 一次规划操作的中立停止原因;Map 与 CoarsePath 分别映射为自己的结果状态。 +internal enum PlanningOperationStopReason +{ + /// 预算仍可继续执行。 + None, + /// 调用方取消了本次操作。 + Cancelled, + /// 从预算创建起已经达到总超时。 + TimedOut, +} + +/// +/// 以单调时钟统一控制一次规划操作的取消和总超时。 +/// 本类不依赖 Map 或 CoarsePath,供两层在长循环检查同一个停止条件。 +/// +internal sealed class PlanningOperationBudget +{ + private const int CheckIntervalWorkItems = 256; + + private readonly CancellationToken _cancellationToken; + private readonly Stopwatch _stopwatch; + private readonly TimeSpan _timeout; + private readonly bool _hasTimeout; + + /// + /// 创建具有总超时的操作预算。 + /// 参数:cancellationToken 为调用方停止请求;timeout 必须非负,且从构造时开始计时。 + /// + internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout) + { + if (timeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout)); + _cancellationToken = cancellationToken; + _timeout = timeout; + _hasTimeout = true; + _stopwatch = Stopwatch.StartNew(); + } + + private PlanningOperationBudget(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + _timeout = TimeSpan.Zero; + _hasTimeout = false; + _stopwatch = Stopwatch.StartNew(); + } + + /// 创建只响应取消、不限制总耗时的兼容预算。 + internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken) + { + return new PlanningOperationBudget(cancellationToken); + } + + /// 从预算创建到当前的单调经过时间。 + internal TimeSpan Elapsed { get { return _stopwatch.Elapsed; } } + + /// 立即查询当前停止原因;取消优先于超时。 + internal PlanningOperationStopReason GetStopReason() + { + if (_cancellationToken.IsCancellationRequested) return PlanningOperationStopReason.Cancelled; + return _hasTimeout && _stopwatch.Elapsed >= _timeout + ? PlanningOperationStopReason.TimedOut + : PlanningOperationStopReason.None; + } + + /// + /// 在循环工作单元中按固定上限检查预算。 + /// 参数:workItemCount 为当前循环共享的已处理单元计数;首次及每 256 个单元检查一次。 + /// + internal PlanningOperationStopReason CheckEvery(ref int workItemCount) + { + workItemCount++; + return workItemCount == 1 || workItemCount % CheckIntervalWorkItems == 0 + ? GetStopReason() + : PlanningOperationStopReason.None; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs b/ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs new file mode 100644 index 0000000..7c41b97 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/Utils/UnitConverter.cs @@ -0,0 +1,11 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.Utils; + +public static class UnitConverter +{ + public static double MillimetersToMeters(double value) { return value / 1000d; } + public static double MetersToMillimeters(double value) { return value * 1000d; } + public static double DegreesToRadians(double value) { return value * Math.PI / 180d; } + public static double RadiansToDegrees(double value) { return value * 180d / Math.PI; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs index 5eae16b..c741578 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs @@ -319,7 +319,7 @@ internal static class TrajectoryObservationMovementTestRunner sessionId, bootstrap, controller, settings, obstacleSnapshotVersion); var dynamicSnapshotBuilder = new TrajectoryObservationDynamicSnapshotBuilder(); long visualizationSequence = 0L; - bool gearSwitchWaitingPrinted = false; + int lastLoggedWaitingSegmentIndex = -1; while (true) { @@ -335,18 +335,17 @@ internal static class TrajectoryObservationMovementTestRunner TrajectoryObservationCharts charts = observation.PublishedTrajectory == null ? null : TrajectoryObservationCharts.Build(observation.PublishedTrajectory, segment, - settings.MapPaddingMeters); - TrajectoryObservationRuntimeState runtimeState = TrajectoryObservationRuntimeState.Create( - now, observation.PublishedTrajectory); + effectiveConfiguration.Frenet.MaximumProjectionDistanceMeters); + TrajectoryObservationRuntimeState runtimeState = TrajectoryObservationRuntimeState.Create(controller.SegmentState); DrawIfCurrent(sessionId, bootstrap, observation, charts, runtimeState, diagnostic); webPublisher?.TryPublish(now, () => dynamicSnapshotBuilder.Build( Interlocked.Increment(ref visualizationSequence), tick, controller.ActiveSegment, controller.PreviousTrajectoryForVisualization, bootstrap.Vehicle, controller.CreateEffectiveConfigurationSnapshot())); - if (runtimeState.WaitingAtGearSwitch && !gearSwitchWaitingPrinted) + if (runtimeState.WaitingAtGearSwitch && segment.SegmentIndex != lastLoggedWaitingSegmentIndex) { LogIfCurrent(sessionId, runtimeState.WorldNotice); - gearSwitchWaitingPrinted = true; + lastLoggedWaitingSegmentIndex = segment.SegmentIndex; } if (tick.PlanningStarted) @@ -360,6 +359,11 @@ internal static class TrajectoryObservationMovementTestRunner TrajectoryObservationDiagnostics.CreateCurveReport(charts, observation.PublishedTrajectory).Text); } } + if (tick.SegmentState.Phase == TrajectoryObservationSegmentPhase.Completed) + { + LogIfCurrent(sessionId, "all direction segments completed; observation session ended."); + break; + } } } diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs index f5fbd8a..8f2e174 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs @@ -195,10 +195,14 @@ public sealed class TrajectoryObservationController private EmPlanningCoordinator coordinator; private TrajectoryExecutor executor; private EmTrajectory previousTrajectoryForVisualization; + private EmTrajectory activeFullDirectionTrajectory; + private EmPlanningCoordinator pendingFullDirectionCoordinator; private readonly string sessionId; private long cycleId; private int plannedSegmentIndex; + private int pendingFullDirectionSegmentIndex = -1; private bool planAttemptedForActiveSegment; + private bool pendingFullDirectionPlanAttempted; public TrajectoryObservationController(TrajectoryObservationBootstrapResult bootstrap, TrajectoryObservationSettings settings, IEmPlanningService planningService, string sessionId) @@ -226,7 +230,9 @@ public sealed class TrajectoryObservationController this.sessionId = sessionId; } - public EmTrajectory PublishedTrajectory => coordinator.PublishedTrajectory; + public EmTrajectory PublishedTrajectory => settings.PlanningScope == EmPlanningScope.FullDirectionSegment + ? activeFullDirectionTrajectory + : coordinator.PublishedTrajectory; public DirectionSegmentView ActiveSegment => bootstrap.Segments[segmentTracker.State.ActiveSegmentIndex]; @@ -253,37 +259,97 @@ public sealed class TrajectoryObservationController plannedSegmentIndex = ActiveSegment.SegmentIndex; planAttemptedForActiveSegment = false; } + if (TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex)) + { + if (pendingFullDirectionSegmentIndex != pendingSegmentIndex) + return true; + return !pendingFullDirectionPlanAttempted; + } return !planAttemptedForActiveSegment; } return coordinator.ShouldStartCycle(now); } - public Task StartCycle(DateTimeOffset now, VehicleMotionState state, + public async Task StartCycle(DateTimeOffset now, VehicleMotionState state, CancellationToken cancellationToken) { if (state == null) throw new ArgumentNullException(nameof(state)); - planAttemptedForActiveSegment = true; - plannedSegmentIndex = ActiveSegment.SegmentIndex; + int targetSegmentIndex = ActiveSegment.SegmentIndex; + int pendingSegmentIndex = -1; + bool planningPendingFullDirection = settings.PlanningScope == EmPlanningScope.FullDirectionSegment && + TryGetPendingFullDirectionSegmentIndex(out pendingSegmentIndex); + EmPlanningCoordinator targetCoordinator; + EmTrajectory previousTrajectory; + if (planningPendingFullDirection) + { + targetSegmentIndex = pendingSegmentIndex; + if (pendingFullDirectionSegmentIndex != targetSegmentIndex || pendingFullDirectionCoordinator == null) + { + pendingFullDirectionSegmentIndex = targetSegmentIndex; + pendingFullDirectionPlanAttempted = false; + pendingFullDirectionCoordinator = new EmPlanningCoordinator(planningService); + } + pendingFullDirectionPlanAttempted = true; + targetCoordinator = pendingFullDirectionCoordinator; + previousTrajectory = null; + } + else + { + planAttemptedForActiveSegment = true; + plannedSegmentIndex = ActiveSegment.SegmentIndex; + targetCoordinator = coordinator; + previousTrajectory = coordinator.PublishedTrajectory; + } + long currentCycleId = Interlocked.Increment(ref cycleId); - EmTrajectory previousTrajectory = coordinator.PublishedTrajectory; var request = new EmPlanningRequest( bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, configuration, - ActiveSegment.SegmentIndex, previousTrajectory, now, now, + targetSegmentIndex, previousTrajectory, now, now, sessionId + "-trajectory-" + currentCycleId, sessionId + "-reference", previousTrajectory?.Metadata.TrajectoryId ?? string.Empty, EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope); - return coordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken); + PlanningCycleResult result = await targetCoordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken) + .ConfigureAwait(false); + if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment && result.Published && + result.Result.Trajectory != null && !planningPendingFullDirection) + { + activeFullDirectionTrajectory = result.Result.Trajectory; + } + return result; } public bool TryAdvanceSegment(DateTimeOffset now, VehicleMotionState state) { if (state == null) throw new ArgumentNullException(nameof(state)); - TrajectoryObservationSegmentUpdate update = segmentTracker.Update(now, state, coordinator.PublishedTrajectory); + if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment && + segmentTracker.State.Phase == TrajectoryObservationSegmentPhase.WaitingForDirection && + !HasPendingFullDirectionTrajectory()) + { + return false; + } + + EmTrajectory activeTrajectory = PublishedTrajectory; + TrajectoryObservationSegmentUpdate update = segmentTracker.Update(now, state, activeTrajectory); if (!update.Advanced) return false; + if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment) + { + EmTrajectory pendingTrajectory = pendingFullDirectionCoordinator.PublishedTrajectory; + previousTrajectoryForVisualization = activeTrajectory; + activeFullDirectionTrajectory = RebaseEffectiveAt(pendingTrajectory, now); + coordinator = pendingFullDirectionCoordinator; + pendingFullDirectionCoordinator = null; + pendingFullDirectionSegmentIndex = -1; + pendingFullDirectionPlanAttempted = false; + executor = new TrajectoryExecutor(configuration); + plannedSegmentIndex = segmentTracker.State.ActiveSegmentIndex; + planAttemptedForActiveSegment = true; + return true; + } + previousTrajectoryForVisualization = coordinator.PublishedTrajectory; coordinator = new EmPlanningCoordinator(planningService); executor = new TrajectoryExecutor(configuration); @@ -296,7 +362,7 @@ public sealed class TrajectoryObservationController { if (state == null) throw new ArgumentNullException(nameof(state)); - EmTrajectory trajectory = coordinator.PublishedTrajectory; + EmTrajectory trajectory = PublishedTrajectory; if (trajectory == null) return new TrajectoryObservationObservation(now, state, null, null, null, null); @@ -313,6 +379,43 @@ public sealed class TrajectoryObservationController return new TrajectoryObservationObservation(now, state, trajectory, executorState.SelectedPoint, command, executorState); } + + private bool TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex) + { + pendingSegmentIndex = -1; + if (settings.PlanningScope != EmPlanningScope.FullDirectionSegment || + segmentTracker.State.Phase != TrajectoryObservationSegmentPhase.WaitingForDirection || + segmentTracker.State.ActiveSegmentIndex + 1 >= bootstrap.Segments.Count) + { + return false; + } + pendingSegmentIndex = segmentTracker.State.ActiveSegmentIndex + 1; + return true; + } + + private bool HasPendingFullDirectionTrajectory() + { + if (!TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex) || + pendingFullDirectionSegmentIndex != pendingSegmentIndex || pendingFullDirectionCoordinator == null) + { + return false; + } + + EmTrajectory pending = pendingFullDirectionCoordinator.PublishedTrajectory; + DirectionSegmentView segment = bootstrap.Segments[pendingSegmentIndex]; + return pending != null && pending.Metadata.SegmentIndex == segment.SegmentIndex && + pending.Metadata.Direction == segment.Direction; + } + + private static EmTrajectory RebaseEffectiveAt(EmTrajectory source, DateTimeOffset effectiveAtUtc) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + EmTrajectoryMetadata metadata = source.Metadata; + return new EmTrajectory(new EmTrajectoryMetadata(metadata.TrajectoryId, metadata.GeneratedAtUtc, effectiveAtUtc, + metadata.MapSnapshotId, metadata.ReferencePathId, metadata.VehicleStateSequenceId, + metadata.PreviousTrajectoryId, metadata.SegmentIndex, metadata.Direction, metadata.TerminalType, + metadata.LongitudinalMode, metadata.PlanningScope), source.Points); + } } public sealed class TrajectoryObservationLoopTick @@ -428,7 +531,7 @@ public static class TrajectoryObservationSessionLifecycle public sealed class TrajectoryObservationRuntimeState { public const string GearSwitchWaitingNotice = - "等待真实档位/方向确认;观察模式不会推进下一方向段"; + "等待真实档位/方向确认;观察模式不会激活下一方向段"; private TrajectoryObservationRuntimeState(bool waitingAtGearSwitch) { @@ -440,6 +543,14 @@ public sealed class TrajectoryObservationRuntimeState public string WorldNotice { get; } + public static TrajectoryObservationRuntimeState Create(TrajectoryObservationSegmentState segmentState) + { + if (segmentState == null) throw new ArgumentNullException(nameof(segmentState)); + bool waiting = segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForStop || + segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForDirection; + return new TrajectoryObservationRuntimeState(waiting); + } + public static TrajectoryObservationRuntimeState Create(DateTimeOffset now, EmTrajectory trajectory) { if (trajectory == null || trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch) diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs index 7606d69..bb809a6 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs @@ -106,7 +106,7 @@ public sealed class TrajectoryObservationSegmentTracker return CreateUpdate(false, true, "方向段已完成,忽略后续观察样本。"); DirectionSegmentView current = segments[State.ActiveSegmentIndex]; - if (!IsEligibleGearSwitchTerminal(now, measuredState, trajectory, current)) + if (!IsEligibleTerminal(now, measuredState, trajectory, current)) { if (State.Phase == TrajectoryObservationSegmentPhase.Planning) return CreateUpdate(false, false, "等待当前方向段的有效换向终点轨迹。"); @@ -167,11 +167,13 @@ public sealed class TrajectoryObservationSegmentTracker (!lastUpdateAtUtc.HasValue || now >= lastUpdateAtUtc.Value); } - private bool IsEligibleGearSwitchTerminal(DateTimeOffset now, VehicleMotionState measuredState, + private bool IsEligibleTerminal(DateTimeOffset now, VehicleMotionState measuredState, EmTrajectory trajectory, DirectionSegmentView current) { if (trajectory == null || trajectory.Metadata.SegmentIndex != current.SegmentIndex || - trajectory.Metadata.Direction != current.Direction || trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch) + trajectory.Metadata.Direction != current.Direction || + (trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch && + trajectory.Metadata.TerminalType != EmTerminalType.Goal)) return false; EmTrajectoryPoint terminalPoint = trajectory.Points[trajectory.Points.Count - 1]; @@ -182,6 +184,8 @@ public sealed class TrajectoryObservationSegmentTracker if (!projector.TryProject(measuredState.Pose, current, current.LengthMeters, current.LengthMeters, settings.GearSwitchProjectionToleranceMeters, current.LengthMeters, out _)) return false; + if (trajectory.Metadata.TerminalType == EmTerminalType.Goal) + return State.ActiveSegmentIndex + 1 >= segments.Count; if (State.ActiveSegmentIndex + 1 >= segments.Count) return true; diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs index acfec3d..c569baa 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs @@ -17,10 +17,12 @@ internal static class EmPlanningServiceChecks VerifiesForwardReverseAndBoundarySuccessesAreDeterministic(); VerifiesServicePublishesRollingApproachAndExactStopModes(); VerifiesFullScopePublishesItsRequestScope(); + VerifiesFullScopeAdmitsOnlyTheTrueSegmentStart(); VerifiesRequestAndStateFailuresPublishNoTrajectory(); VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory(); VerifiesNoProgressPublishesNoTrajectory(); VerifiesTimeoutFallbackAndCancellationSemantics(); + VerifiesLsAndStShareOneSolveBudget(); VerifiesPublicationFailureAndDebugIsolation(); VerifiesPublicationGateStatusMappings(); } @@ -123,6 +125,32 @@ internal static class EmPlanningServiceChecks "full publication reaches the real segment boundary"); } + private static void VerifiesFullScopeAdmitsOnlyTheTrueSegmentStart() + { + EmPlanningRequest uRequest = CreateRequest(TravelDirection.Forward, 0d, false, false, + CreateAllForwardUReferencePath(), CreateMap(false, 12d), EmPlanningScope.FullDirectionSegment); + DateTimeOffset capturedAt = uRequest.RequestedAtUtc; + uRequest = ReplaceState(uRequest, new VehicleMotionState( + new Pose2D(5d, 0.4d, Math.PI), 0d, 0d, capturedAt, 8L)); + + EmPlanningResult laterBranch = new EmPlanningService( + new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(uRequest, CancellationToken.None); + + VerifyFailure(laterBranch, EmPlanningStatus.ProjectionFailed, + "FullDirection later U branch is not an admissible segment start"); + + EmPlanningRequest oppositeHeading = CreateRequest(TravelDirection.Forward, 0d, false, false, + planningScope: EmPlanningScope.FullDirectionSegment); + oppositeHeading = ReplaceState(oppositeHeading, new VehicleMotionState( + new Pose2D(0d, 0d, Math.PI), 0d, 0d, oppositeHeading.RequestedAtUtc, 9L)); + + EmPlanningResult foldedHeading = new EmPlanningService( + new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(oppositeHeading, CancellationToken.None); + + VerifyFailure(foldedHeading, EmPlanningStatus.ProjectionFailed, + "FullDirection opposite heading is rejected before tangent slope conversion"); + } + private static void ConfigureFiveMeterWindowAndTwoSecondHorizon(EmPlannerConfiguration configuration) { configuration.Scheduling.DistanceHorizonMeters = 5d; @@ -396,6 +424,38 @@ internal static class EmPlanningServiceChecks VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(request, cancellation.Token), EmPlanningStatus.Cancelled, "cancellation"); } + + using (var cancellation = new CancellationTokenSource()) + { + EmPlanningRequest publicationRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); + publicationRequest.Configuration.Solver.NativeVerbose = true; + var service = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success), + new CancellingPublicationDebugSink(cancellation)); + + EmPlanningResult cancelledBeforePublication = service.Plan(publicationRequest, cancellation.Token); + + VerifyFailure(cancelledBeforePublication, EmPlanningStatus.Cancelled, + "cancellation immediately before service publication"); + } + } + + private static void VerifiesLsAndStShareOneSolveBudget() + { + EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); + request.Configuration.Scheduling.SolverTimeoutSeconds = 1d; + request.Configuration.Solver.MaximumOuterIterations = 1; + var solver = new ScriptedPipelineSolver(PipelineSolverMode.Success, + lateralSolveDelay: TimeSpan.FromMilliseconds(200d)); + + EmPlanningResult result = new EmPlanningService(solver).Plan(request, CancellationToken.None); + + Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, + "shared LS/ST budget test publishes a validated trajectory"); + Verification.True(solver.LateralSolveBudgets.Count > 0 && solver.LongitudinalSolveBudgets.Count > 0, + "shared LS/ST budget test reached both optimizers"); + Verification.True(solver.LongitudinalSolveBudgets[0] < + solver.LateralSolveBudgets[0] - TimeSpan.FromMilliseconds(100d), + "ST receives only the LS/ST budget remaining after the delayed LS solve"); } private static void VerifiesNoProgressPublishesNoTrajectory() @@ -616,6 +676,28 @@ internal static class EmPlanningServiceChecks new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); } + private static PathSmoothingResult CreateAllForwardUReferencePath() + { + var points = new List + { + new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, TravelDirection.Forward, + 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothedPathPoint(10d, 0d, 0d, 0d, 10d, TravelDirection.Forward, + 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothedPathPoint(10d, 0.4d, Math.PI, Math.PI, 10.4d, TravelDirection.Forward, + 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + new SmoothedPathPoint(0d, 0.4d, Math.PI, Math.PI, 20.4d, TravelDirection.Forward, + 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), + }; + var segments = new List + { + new SmoothedPathSegment(0, TravelDirection.Forward, 0, points.Count - 1, false, false), + }; + var metrics = new PathQualityMetrics(true, 20.4d, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); + return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, + new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); + } + private static PlanningGridMap CreateMap(bool blockStart, double halfExtentMeters = 3d) { IMapObstacleSource[] sources = blockStart @@ -651,16 +733,20 @@ internal static class EmPlanningServiceChecks private readonly PipelineSolverMode mode; private readonly PlanningGridMap? mapToCorrupt; private readonly IReadOnlyList? strictFullPrimal; + private readonly TimeSpan lateralSolveDelay; private int longitudinalCallCount; public QuadraticProgram? LastLongitudinalProblem { get; private set; } + public List LateralSolveBudgets { get; } = new(); + public List LongitudinalSolveBudgets { get; } = new(); public ScriptedPipelineSolver(PipelineSolverMode mode, PlanningGridMap? mapToCorrupt = null, - IReadOnlyList? strictFullPrimal = null) + IReadOnlyList? strictFullPrimal = null, TimeSpan? lateralSolveDelay = null) { this.mode = mode; this.mapToCorrupt = mapToCorrupt; this.strictFullPrimal = strictFullPrimal; + this.lateralSolveDelay = lateralSolveDelay.GetValueOrDefault(); } public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, @@ -671,12 +757,16 @@ internal static class EmPlanningServiceChecks return Result(QpSolveStatus.SolverUnavailable, Array.Empty()); if (!longitudinal) { + LateralSolveBudgets.Add(settings.TimeLimit); + if (lateralSolveDelay > TimeSpan.Zero) + Thread.Sleep(lateralSolveDelay); if (mode == PipelineSolverMode.LateralInfeasible) return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); if (mode == PipelineSolverMode.TimeoutWithoutFallback) return Result(QpSolveStatus.TimeLimit, Array.Empty()); return Result(QpSolveStatus.Solved, new double[problem.VariableCount]); } + LongitudinalSolveBudgets.Add(settings.TimeLimit); LastLongitudinalProblem = problem; if (mode == PipelineSolverMode.LongitudinalInfeasible) return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); @@ -928,7 +1018,10 @@ internal static class EmPlanningServiceChecks if (problem.VariableCount < 7 || (problem.VariableCount + 1) % 4 != 0) return false; int knotCount = (problem.VariableCount + 1) / 4; - return problem.ConstraintCount >= 8 * knotCount - 2; + var layout = new LongitudinalVariableLayout(knotCount); + return TryReadFixedVariable(problem, layout.S(0), out _) && + TryReadFixedVariable(problem, layout.U(0), out _) && + TryReadFixedVariable(problem, layout.A(0), out _); } private static double ReadFixedVariable(QuadraticProgram problem, int variable) @@ -980,4 +1073,20 @@ internal static class EmPlanningServiceChecks throw new InvalidOperationException("debug sink failure"); } } + + private sealed class CancellingPublicationDebugSink : IEmPlannerDebugSink + { + private readonly CancellationTokenSource cancellation; + + public CancellingPublicationDebugSink(CancellationTokenSource cancellation) + { + this.cancellation = cancellation ?? throw new ArgumentNullException(nameof(cancellation)); + } + + public void Write(string message) + { + if (string.Equals(message, "world-space publication validation succeeded", StringComparison.Ordinal)) + cancellation.Cancel(); + } + } } diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs index 9651709..18f4f87 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs @@ -60,3 +60,29 @@ internal sealed class FakeQpSolver : IQpSolver return _results.Dequeue(); } } + +internal sealed class CancellingQpSolver : IQpSolver +{ + private readonly IQpSolver inner; + private readonly CancellationTokenSource cancellation; + private readonly int cancelAfterSolveCount; + private int solveCount; + + public CancellingQpSolver(IQpSolver inner, CancellationTokenSource cancellation, int cancelAfterSolveCount = 1) + { + this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); + this.cancellation = cancellation ?? throw new ArgumentNullException(nameof(cancellation)); + if (cancelAfterSolveCount <= 0) throw new ArgumentOutOfRangeException(nameof(cancelAfterSolveCount)); + this.cancelAfterSolveCount = cancelAfterSolveCount; + } + + public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, + CancellationToken cancellationToken) + { + QpSolveResult result = inner.Solve(problem, settings, warmStart, cancellationToken); + solveCount++; + if (solveCount == cancelAfterSolveCount) + cancellation.Cancel(); + return result; + } +} diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs index 671e9d9..0f69da2 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs @@ -16,6 +16,8 @@ internal static class LateralIntegrationChecks { VerifiesValidatedCandidateSurvivesLaterTimeout(); VerifiesInvalidVectorsAndInaccurateResidualsNeverBecomeFallbacks(); + VerifiesCancellationAfterStrictCandidateNeverPublishesFallback(); + VerifiesRejectedCandidateAdvancesTheNextLateralLinearization(); VerifiesTrustRegionWarmStartAndOuterIterationLimit(); VerifiesCancellationAndTimeoutWithoutCandidate(); VerifiesLateralPlannerDelegatesToTheSequentialOptimizer(); @@ -150,6 +152,47 @@ internal static class LateralIntegrationChecks "SolvedInaccurate still requires full independent lateral validation"); } + private static void VerifiesCancellationAfterStrictCandidateNeverPublishesFallback() + { + LateralPlanningInput input = CreateInput(); + double[] valid = CreatePrimal(input, 0.02d); + using var cancellation = new CancellationTokenSource(); + var solver = new CancellingQpSolver( + new FakeQpSolver(Result(QpSolveStatus.Solved, valid, 10d)), cancellation); + + LateralPlanningResult result = new SequentialConvexOptimizer(solver).Optimize(input, cancellation.Token); + + Verification.Equal(EmPlanningStatus.Cancelled, result.Status, + "cancellation after a strict LS candidate is never fallback success"); + Verification.True(result.Path == null, "cancelled LS result exposes no path"); + } + + private static void VerifiesRejectedCandidateAdvancesTheNextLateralLinearization() + { + LateralPlanningInput input = CreateInput(0.10d); + double[] strict = CreatePrimal(input, 0.01d); + double[] rejected = CreatePrimal(input, 0.02d); + var solver = new FakeQpSolver(new[] + { + Result(QpSolveStatus.Solved, strict, 10d), + Result(QpSolveStatus.Solved, rejected, 9d), + Result(QpSolveStatus.TimeLimit, Array.Empty(), 9d), + }); + + LateralPlanningResult result = new SequentialConvexOptimizer(solver).Optimize(input, CancellationToken.None); + + Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status, + "later timeout preserves the earlier strict candidate"); + var layout = new LateralVariableLayout(input.ReferenceStations.Count); + Verification.NearlyEqual(0.02d, solver.WarmStarts[2][layout.L(1)], + "the next LS QP warm-starts from the rejected but parseable candidate"); + FindSingleVariableBounds(solver.Problems[2], layout.L(1), out double lower, out double upper); + Verification.NearlyEqual(-0.03d, lower, + "the next LS trust region is centered on the rejected candidate"); + Verification.NearlyEqual(0.07d, upper, + "the next LS trust region is centered on the rejected candidate"); + } + private static void VerifiesTrustRegionWarmStartAndOuterIterationLimit() { LateralPlanningInput input = CreateInput(); @@ -178,7 +221,10 @@ internal static class LateralIntegrationChecks var limitSolver = new FakeQpSolver(limitResults); LateralPlanningResult limited = new SequentialConvexOptimizer(limitSolver).Optimize(input, CancellationToken.None); Verification.Equal(5, limitSolver.SolveCallCount, "outer loop stops after at most five QP calls"); - Verification.Equal(EmPlanningStatus.Success, limited.Status, "last feasible candidate succeeds at outer iteration limit"); + Verification.Equal(EmPlanningStatus.SuccessWithFallback, limited.Status, + "last strict candidate is reported as fallback at outer iteration limit"); + Verification.True(limited.FailureReason.Length > 0, + "outer iteration fallback preserves a non-empty diagnostic"); } private static void VerifiesCancellationAndTimeoutWithoutCandidate() @@ -319,7 +365,7 @@ internal static class LateralIntegrationChecks Verification.True(Math.Abs(leftValues[index] - rightValues[index]) <= 1e-10d, name + " value " + index); } - private static LateralPlanningInput CreateInput() + private static LateralPlanningInput CreateInput(double maximumVehicleCurvature = 1d) { var points = new List { @@ -341,7 +387,7 @@ internal static class LateralIntegrationChecks LengthMeters = 0.1d, WidthMeters = 0.1d, SafetyMarginMeters = 0d, - MaximumCurvaturePerMeter = 1d, + MaximumCurvaturePerMeter = maximumVehicleCurvature, }; return new LateralPlanningInput(segment, corridor, new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0d, 0d, 0d), diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs index 5539d41..a0a7146 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs @@ -18,6 +18,7 @@ internal static class LateralModelChecks VerifiesPlanningInputBoundariesAndDefensiveCopies(); VerifiesLateralResultPublicationContract(); VerifiesNormalizedObjectiveAndHardConstraints(); + VerifiesVehicleCurvatureIsAHardQpConstraint(); VerifiesAllNamedCostScales(); VerifiesEmptyHardBoundIntersectionFailsBeforeSolve(); VerifiesFakeSolverCapturesTheNeutralQpBoundary(); @@ -245,6 +246,29 @@ internal static class LateralModelChecks "Frenet denominator is intersected as a finite hard lateral bound"); } + private static void VerifiesVehicleCurvatureIsAHardQpConstraint() + { + EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); + const double maximumVehicleCurvature = 0.25d; + LateralPlanningInput input = CreateModelInput(EmTerminalType.RollingSafetyStop, configuration, + Array.Empty(), maximumVehicleCurvature: maximumVehicleCurvature); + LateralCandidate linearization = LateralCandidate.Integrate(input.ReferenceStations, 0d, 0d, 0d, + new[] { 0d, 0d }); + + Verification.True(CreateConstraintBuilder().TryBuild(input, linearization, out QuadraticProgram problem, + out string failureReason), "curvature-constrained QP builds: " + failureReason); + + var layout = new LateralVariableLayout(input.ReferenceStations.Count); + Verification.Equal(8 * layout.StationCount - 2, problem.ConstraintCount, + "each lateral station adds one curvature hard-constraint row"); + for (int station = 0; station < layout.StationCount; station++) + { + Verification.True(HasBound(problem, new Dictionary { { layout.DDL(station), 1d } }, + -maximumVehicleCurvature, maximumVehicleCurvature), + "straight-reference curvature is a hard DDL bound at station " + station); + } + } + private static void VerifiesEmptyHardBoundIntersectionFailsBeforeSolve() { EmPlannerConfiguration configuration = CreateUnitScaleConfiguration(); @@ -550,6 +574,21 @@ internal static class LateralModelChecks return false; } + private static bool HasBound(QuadraticProgram problem, IReadOnlyDictionary expected, + double lower, double upper) + { + for (int row = 0; row < problem.ConstraintCount; row++) + { + if (RowMatches(problem.ConstraintMatrix, row, expected) && + Math.Abs(problem.LowerBounds[row] - lower) <= 1e-12d && + Math.Abs(problem.UpperBounds[row] - upper) <= 1e-12d) + { + return true; + } + } + return false; + } + private static bool RowMatches(SparseCscMatrix matrix, int targetRow, IReadOnlyDictionary expected) { var actual = new Dictionary(); diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs index 395ce2a..889076c 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs @@ -554,6 +554,18 @@ internal static class LongitudinalIntegrationChecks Verification.Equal(0, cancellationSolver.SolveCallCount, "cancelled ST does not call the QP solver"); } + using (var cancellation = new CancellationTokenSource()) + { + var solver = new CancellingQpSolver( + new FakeQpSolver(Result(QpSolveStatus.Solved, ToPrimal(valid), 1d)), cancellation); + LongitudinalPlanningResult cancelledAfterStrictCandidate = new SequentialLongitudinalOptimizer(solver).Optimize( + input, cancellation.Token); + Verification.Equal(EmPlanningStatus.Cancelled, cancelledAfterStrictCandidate.Status, + "cancellation after a strict ST candidate is never fallback success"); + Verification.True(cancelledAfterStrictCandidate.Candidate == null, + "cancelled ST result exposes no candidate"); + } + var infeasibleSolver = new FakeQpSolver(Result(QpSolveStatus.PrimalInfeasible, Array.Empty(), 1d)); LongitudinalPlanningResult infeasible = new SequentialLongitudinalOptimizer(infeasibleSolver).Optimize(input, CancellationToken.None); diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs index c3634b3..e755fb1 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs @@ -12,6 +12,7 @@ internal static class TrajectoryChecks public static void Run() { VerifiesForwardFieldsExactTerminalAndHold(); + VerifiesSegmentLocalReferenceStationIsPublished(); VerifiesReverseTravelVelocityAndUnwrappedYaw(); VerifiesPublishedListsAreImmutable(); VerifiesRollingTrajectoryHasNoSyntheticStopTail(); @@ -49,6 +50,30 @@ internal static class TrajectoryChecks "reverse yaw interpolation unwraps across the pi boundary"); } + private static void VerifiesSegmentLocalReferenceStationIsPublished() + { + EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( + CreatePath(TravelDirection.Forward, 0d, 0d), CreateLongitudinalResult(), + CreateMetadata(TravelDirection.Forward, EmTerminalType.Goal)); + + EmTrajectoryPoint terminal = trajectory.Points[trajectory.Points.Count - 1]; + Verification.NearlyEqual(1d, terminal.SegmentLocalS, + "published SegmentLocalS retains the lateral path reference station"); + Verification.NearlyEqual(0.12d, terminal.PathS, + "published PathS remains the longitudinal actual path distance"); + + ValidationContext context = CreateValidationContext(); + EmTrajectory validationTrajectory = CreateValidationTrajectory(TravelDirection.Forward); + EmTrajectoryValidationResult accepted = new EmTrajectoryValidator().Validate(validationTrajectory, context.EmptyMap, + context.Vehicle, context.Configuration, 2, 1d, 0.0055d, EmBoundaryType.Goal); + Verification.True(accepted.IsValid, + "publication validates reference and actual path bounds independently: " + accepted.Message); + EmTrajectoryValidationResult segmentExceeded = new EmTrajectoryValidator().Validate(validationTrajectory, + context.EmptyMap, context.Vehicle, context.Configuration, 2, 0.5d, 0.0055d, EmBoundaryType.Goal); + Verification.Equal(EmTrajectoryValidationFailure.SegmentBoundaryExceeded, segmentExceeded.Failure, + "segment-local reference bound is checked independently of PathS"); + } + private static void VerifiesPublishedListsAreImmutable() { EmTrajectory trajectory = new EmTrajectoryAssembler().Assemble( diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs index 81d7aa3..dee1db2 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs @@ -39,7 +39,8 @@ internal static class TrajectoryObservationChecks VerifiesObserverTicksWhilePlanningIsDelayed(); VerifiesFullDirectionSegmentPlansOncePerActiveSegment(); VerifiesFullDirectionRequestCarriesValidatedScope(); - VerifiesFullDirectionPlansAgainOnlyAfterConfirmedTransition(); + VerifiesFullDirectionPreplansAfterStopAndActivatesAfterConfirmation(); + VerifiesDeterministicSingleForwardObservationSimulation(); VerifiesFailedFullPlanDoesNotAutoRetry(); VerifiesStaticSnapshotExportsPlanningScope(); VerifiesSessionLayerCleanupDecisions(); @@ -114,7 +115,7 @@ internal static class TrajectoryObservationChecks Verification.True(source.Contains("[MovementTest(name = \"EM轨迹规划观察闭环测试\")]"), "observation MovementTest uses required Chinese display name"); - Verification.Equal("等待真实档位/方向确认;观察模式不会推进下一方向段", + Verification.Equal("等待真实档位/方向确认;观察模式不会激活下一方向段", TrajectoryObservationRuntimeState.GearSwitchWaitingNotice, "observation runtime uses required gear-switch notice"); Verification.True(source.Contains("Console.WriteLine(\"[TrajectoryObserver] \" + text);"), @@ -180,6 +181,10 @@ internal static class TrajectoryObservationChecks Verification.True(normalizedSource.Contains( "VehicleMotionState state = ReadVehicleState();\n DateTimeOffset now = state.CapturedAtUtc;"), "observer host uses the fresh state snapshot time for each observation tick"); + Verification.True(source.Contains("effectiveConfiguration.Frenet.MaximumProjectionDistanceMeters"), + "native observation charts use the EM Frenet projection tolerance rather than map padding"); + Verification.True(source.Contains("tick.SegmentState.Phase == TrajectoryObservationSegmentPhase.Completed"), + "observer session terminates after the final direction segment completes"); Verification.NearlyEqual(0.10d, new TrajectoryObservationSettings().OutputTimeStepSeconds, "observer settings use the ST timestamp-spacing default"); @@ -524,7 +529,7 @@ internal static class TrajectoryObservationChecks "full scope blocks coordinator-cadence restart after the first plan"); } - private static void VerifiesFullDirectionPlansAgainOnlyAfterConfirmedTransition() + private static void VerifiesFullDirectionPreplansAfterStopAndActivatesAfterConfirmation() { DateTimeOffset t0 = new DateTimeOffset(2026, 8, 7, 2, 0, 0, TimeSpan.Zero); var settings = new TrajectoryObservationSettings @@ -550,28 +555,90 @@ internal static class TrajectoryObservationChecks var stoppedState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0, 1L); controller.StartCycle(t0, stoppedState, CancellationToken.None).GetAwaiter().GetResult(); - Verification.True(!controller.ShouldStartCycle(t0.AddSeconds(1d)), - "no replan is armed before a confirmed segment transition"); controller.TryAdvanceSegment(t0, stoppedState); - controller.TryAdvanceSegment(t0.AddSeconds(0.21d), - new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, t0.AddSeconds(0.21d), 2L)); - Verification.True(!controller.ShouldStartCycle(t0.AddSeconds(1d)), - "stop hold and direction waiting do not reset the one-shot flag"); + DateTimeOffset stoppedAt = t0.AddSeconds(0.21d); + var heldStoppedState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, stoppedAt, 2L); + controller.TryAdvanceSegment(stoppedAt, heldStoppedState); + Verification.True(controller.ShouldStartCycle(stoppedAt), + "the next full direction segment is planned after the real stop hold, before direction evidence"); + + PlanningCycleResult pendingCycle = controller.StartCycle(stoppedAt, heldStoppedState, CancellationToken.None) + .GetAwaiter().GetResult(); + Verification.True(pendingCycle.Published, "the stopped-state pending segment plan is independently publishable"); + Verification.Equal(2, planningService.Requests.Count, + "the next segment is planned exactly once from the stopped state"); + Verification.Equal(1, planningService.Requests[1].SegmentIndex, + "the stopped-state pending request targets segment N+1"); + Verification.Equal(heldStoppedState.SequenceId, planningService.Requests[1].VehicleState.SequenceId, + "the pending segment request uses the real stopped vehicle state"); + Verification.True(planningService.Requests[1].PreviousTrajectory == null, + "a pending direction segment never reuses the old direction trajectory as an EM seed"); + Verification.Equal(0, controller.ActiveSegment.SegmentIndex, + "planning N+1 does not replace the old segment before direction confirmation"); + Verification.True(ReferenceEquals(forwardGearTrajectory, controller.PublishedTrajectory), + "the old segment trajectory remains active while N+1 waits for confirmation"); var reverseState = new VehicleMotionState(new Pose2D(1d, 0d, 0d), -0.03d, null, t0.AddSeconds(0.22d), 3L); Verification.True(controller.TryAdvanceSegment(t0.AddSeconds(0.22d), reverseState), "confirmed N to N+1 transition succeeds"); - Verification.True(controller.ShouldStartCycle(t0.AddSeconds(0.23d)), - "one-shot planning is rearmed only after the confirmed transition"); - - controller.StartCycle(t0.AddSeconds(0.23d), reverseState, CancellationToken.None).GetAwaiter().GetResult(); + Verification.Equal(1, controller.ActiveSegment.SegmentIndex, + "the pending plan becomes active only after direction confirmation"); + Verification.True(!controller.ShouldStartCycle(t0.AddSeconds(0.23d)), + "confirmed activation does not trigger a duplicate full-segment plan"); Verification.Equal(2, planningService.Requests.Count, - "the next active direction segment receives exactly one new plan"); - Verification.Equal(1, planningService.Requests[1].SegmentIndex, - "the new full-direction plan targets segment N+1"); - Verification.Equal(EmPlanningScope.FullDirectionSegment, planningService.Requests[1].PlanningScope, - "the new full-direction plan keeps the full scope"); + "the pending plan is reused at activation instead of planning N+1 twice"); + Verification.Equal(reverseState.CapturedAtUtc, controller.PublishedTrajectory.Metadata.EffectiveAtUtc, + "pending trajectory effective time is rebased to its real activation time"); + } + + private static void VerifiesDeterministicSingleForwardObservationSimulation() + { + DateTimeOffset t0 = new DateTimeOffset(2026, 8, 8, 0, 0, 0, TimeSpan.Zero); + var settings = new TrajectoryObservationSettings + { + PlanningScope = EmPlanningScope.FullDirectionSegment, + }; + CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( + new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings, + Array.Empty(), 0L); + TrajectoryObservationBootstrapResult baseBootstrap = new TrajectoryObservationBootstrapper() + .Bootstrap(job, CancellationToken.None); + Verification.True(baseBootstrap.Succeeded, "deterministic single-forward simulation bootstrap succeeds"); + TrajectoryObservationBootstrapResult bootstrap = TrajectoryObservationBootstrapResult.Success( + baseBootstrap.Job, baseBootstrap.CoarseResult, baseBootstrap.SmoothedPath, new[] + { + CreateDirectionalSegment(0, TravelDirection.Forward, 0d, 1d, false, + EmBoundaryType.None, EmBoundaryType.Goal), + }); + EmTrajectory trajectory = CreateSingleForwardSimulationTrajectory(t0); + var controller = new TrajectoryObservationController(bootstrap, settings, + new FixedTrajectoryPlanningService(trajectory), "single-forward-simulation"); + var loop = new TrajectoryObservationLoop(controller); + + double x = 0d; + double heading = 0d; + var initialState = new VehicleMotionState(new Pose2D(x, 0d, heading), 0d, null, t0, 1L); + controller.StartCycle(t0, initialState, CancellationToken.None).GetAwaiter().GetResult(); + TrajectoryControlCommand command = controller.Observe(t0, initialState).Command; + TrajectoryObservationLoopTick? finalTick = null; + const double deltaSeconds = 0.10d; + for (int step = 1; step <= 10; step++) + { + x += command.SignedLongitudinalVelocity * Math.Cos(heading) * deltaSeconds; + heading += command.YawRate * deltaSeconds; + DateTimeOffset now = t0.AddSeconds(step * deltaSeconds); + var state = new VehicleMotionState(new Pose2D(x, 0d, heading), command.SignedLongitudinalVelocity, + null, now, step + 1L); + finalTick = loop.Tick(now, state, CancellationToken.None); + command = finalTick.Observation.Command; + } + + Verification.NearlyEqual(1d, x, "simulated forward vehicle reaches the real Goal pose from executor commands"); + Verification.Equal(TrajectoryObservationSegmentPhase.Completed, finalTick!.SegmentState.Phase, + "simulated forward Goal completes the observation lifecycle"); + Verification.NearlyEqual(0d, command.SignedLongitudinalVelocity, + "simulated forward Goal receives the terminal zero-speed command"); } private static void VerifiesFailedFullPlanDoesNotAutoRetry() @@ -666,11 +733,24 @@ internal static class TrajectoryObservationChecks effectiveAt.AddSeconds(1d), trajectory); Verification.True(atFinal.WaitingAtGearSwitch, "observer enters gear-switch wait state at final time"); - Verification.Equal("等待真实档位/方向确认;观察模式不会推进下一方向段", atFinal.WorldNotice, + Verification.Equal("等待真实档位/方向确认;观察模式不会激活下一方向段", atFinal.WorldNotice, "observer exposes the exact gear-switch state to the world painter"); Verification.Equal(0, trajectory.Metadata.SegmentIndex, "observer gear-switch wait state remains on segment zero"); + var tracker = new TrajectoryObservationSegmentTracker(CreateDirectionalSegments(), + new TrajectoryObservationSettings(), 0.01d); + TrajectoryObservationRuntimeState planningState = TrajectoryObservationRuntimeState.Create(tracker.State); + Verification.True(!planningState.WaitingAtGearSwitch, + "tracker-planning state does not show a gear-switch wait notice merely because planned time elapsed"); + tracker.Update(effectiveAt.AddSeconds(1d), new VehicleMotionState(new Pose2D(1d, 0d, 0d), 0d, null, + effectiveAt.AddSeconds(1d), 1L), + CreateSegmentTrajectory(effectiveAt, "runtime-forward", 0, TravelDirection.Forward, + EmTerminalType.GearSwitch, EmBoundaryType.GearSwitchApproach, 1d)); + TrajectoryObservationRuntimeState stoppedState = TrajectoryObservationRuntimeState.Create(tracker.State); + Verification.True(stoppedState.WaitingAtGearSwitch, + "actual tracker stop state shows the gear-switch wait notice"); + string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); string presentationSource = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); @@ -780,6 +860,7 @@ internal static class TrajectoryObservationChecks DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 4, 0, 0, TimeSpan.Zero); var settings = new TrajectoryObservationSettings { + PlanningScope = EmPlanningScope.RollingHorizon, DirectionConfirmationSamples = 1, }; CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( @@ -1244,6 +1325,20 @@ internal static class TrajectoryObservationChecks }); } + private static EmTrajectory CreateSingleForwardSimulationTrajectory(DateTimeOffset effectiveAt) + { + var metadata = new EmTrajectoryMetadata("single-forward-simulation", effectiveAt, effectiveAt, 1L, + "single-forward-reference", 1L, string.Empty, 0, TravelDirection.Forward, EmTerminalType.Goal, + EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.FullDirectionSegment); + return new EmTrajectory(metadata, new[] + { + new EmTrajectoryPoint(0d, 0d, 0d, 1d, 0d, 0d, 0, 0d, 0d, + TravelDirection.Forward, EmBoundaryType.None, 0d, 0d), + new EmTrajectoryPoint(1d, 0d, 0d, 0d, 1d, 0d, 0, 1d, 1d, + TravelDirection.Forward, EmBoundaryType.Goal, 0d, 0d), + }); + } + private sealed class FixedTrajectoryPlanningService : IEmPlanningService { private readonly EmTrajectory trajectory; diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs index e1759ca..c204933 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs @@ -17,6 +17,7 @@ internal static class TrajectoryObservationSegmentChecks RejectsNonTerminalOrMismatchedGearTrajectory(); ResetsConfirmationForInvalidDirectionEvidence(); CompletesOneSegmentWithoutIndexingPastTheEnd(); + CompletesSingleForwardGoalTrajectory(); } private static void RejectsHardcodedActiveSegmentIndex() @@ -112,6 +113,22 @@ internal static class TrajectoryObservationSegmentChecks Verification.Equal(0, tracker.State.ActiveSegmentIndex, "completed tracker retains final segment index"); } + private static void CompletesSingleForwardGoalTrajectory() + { + DateTimeOffset t0 = new DateTimeOffset(2026, 8, 6, 3, 30, 0, TimeSpan.Zero); + var tracker = new TrajectoryObservationSegmentTracker(new[] + { + CreateSegment(0, TravelDirection.Forward, 0d, 1d, false, 0d, EmBoundaryType.None, EmBoundaryType.Goal), + }, CreateSettings(), 0.01d); + + TrajectoryObservationSegmentUpdate completed = tracker.Update(t0, StateAtSwitch(0d, t0, 1L), + GoalTerminal(t0)); + + Verification.True(completed.Completed, "single forward Goal trajectory reports completion"); + Verification.Equal(TrajectoryObservationSegmentPhase.Completed, tracker.State.Phase, + "single forward Goal reaches completed without requiring a gear-switch terminal"); + } + private static void AssertConfirmationReset(DateTimeOffset t0, VehicleMotionState invalidState, EmTrajectory invalidTrajectory, string name) { @@ -196,4 +213,16 @@ internal static class TrajectoryObservationSegmentChecks direction, EmBoundaryType.GearSwitchApproach, 0d, 0d), }); } + + private static EmTrajectory GoalTerminal(DateTimeOffset effectiveAtUtc) + { + var metadata = new EmTrajectoryMetadata("goal-terminal-" + effectiveAtUtc.Ticks, effectiveAtUtc, + effectiveAtUtc, 1L, "segment-check", 1L, string.Empty, 0, TravelDirection.Forward, + EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, EmPlanningScope.FullDirectionSegment); + return new EmTrajectory(metadata, new[] + { + new EmTrajectoryPoint(1d, 0d, 0d, 0d, 0d, 0d, 0, 1d, 1d, + TravelDirection.Forward, EmBoundaryType.Goal, 0d, 0d), + }); + } } diff --git a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj new file mode 100644 index 0000000..94e673d --- /dev/null +++ b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/PathSmoothingPngVerificationHost.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0-windows + true + disable + enable + + + + + + diff --git a/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs new file mode 100644 index 0000000..e9d3f9d --- /dev/null +++ b/ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs @@ -0,0 +1,590 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; + +internal static class Program +{ + private static readonly byte[] PngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + private static int Main(string[] arguments) + { + try + { + if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic") + { + VerifyLocalG2Diagnostic(arguments[1], arguments[2]); + Console.WriteLine("Local G2 diagnostic visualization verification completed."); + return 0; + } + if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic") + { + ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]); + return 0; + } + if (arguments.Length == 3 && arguments[0] == "--export-fixtures") + { + ExportFixtureReports(arguments[1], arguments[2]); + return 0; + } + if (arguments.Length == 2 && arguments[0] == "--export-end-to-end") + { + ExportEndToEndReports(arguments[1]); + return 0; + } + Require(arguments.Length == 1 && File.Exists(arguments[0]), "A fixture path is required."); + Verify(arguments[0]); + Console.WriteLine("PNG verification host completed."); + return 0; + } + catch (Exception exception) + { + Console.Error.WriteLine(exception.ToString()); + return 1; + } + } + + private static void ExportFixtureReports(string fixturePath, string outputDirectory) + { + Require(File.Exists(fixturePath), "Fixture path was not found."); + PrintReports(new PathSmoothingComparisonDemo().ExportFixtureReports(fixturePath, outputDirectory)); + } + + private static void ExportEndToEndReports(string outputDirectory) + { + PrintReports(new PathSmoothingComparisonDemo().ExportEndToEndReports(outputDirectory)); + } + + private static void ExportLocalG2Diagnostic(string fixturePath, string evidencePath, string outputDirectory) + { + Require(File.Exists(fixturePath), "Fixture path was not found."); + Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); + SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( + fixturePath, + evidencePath, + outputDirectory); + Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); + Console.WriteLine("Diagnostic report=" + outputDirectory); + } + + private static void VerifyLocalG2Diagnostic(string fixturePath, string evidencePath) + { + Require(File.Exists(fixturePath), "Fixture path was not found."); + Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); + string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-diagnostic-" + Guid.NewGuid().ToString("N")); + string badEvidencePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-bad-" + Guid.NewGuid().ToString("N") + ".json"); + string rejectedOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-rejected-" + Guid.NewGuid().ToString("N")); + string alteredFixturePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-altered-fixture-" + Guid.NewGuid().ToString("N") + ".json"); + string rejectedFixtureOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-fixture-rejected-" + Guid.NewGuid().ToString("N")); + try + { + SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( + fixturePath, + evidencePath, + outputDirectory); + Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); + Require(report.SvgPaths.Count == 5 && report.PngPaths.Count == 5 && File.Exists(report.CsvPath), + "Diagnostic export must publish five SVGs, five PNGs and one CSV."); + Require(Path.GetFileName(report.SvgPaths[4]) == "05-local-g2-diagnostic-candidate.svg", + "Diagnostic SVG stem is incorrect."); + Require(Path.GetFileName(report.PngPaths[4]) == "05-local-g2-diagnostic-candidate.png", + "Diagnostic PNG stem is incorrect."); + string diagnosticSvg = File.ReadAllText(report.SvgPaths[4]); + Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), + "Diagnostic SVG must contain raw and diagnostic samples."); + Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布") && + diagnosticSvg.Contains("严格输出=原始路径"), + "Diagnostic SVG must disclose clearance rejection, publication state, and strict output boundary."); + VerifyDiagnosticPointOverlay(diagnosticSvg, report.PngPaths[6]); + Require(!diagnosticSvg.Contains("violation-cross"), + "Clearance rejection must not be drawn as an obstacle collision."); + Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), + "CSV must retain the normal strict Local G2 row."); + for (int index = 0; index < report.PngPaths.Count; index++) + VerifyPng(File.ReadAllBytes(report.PngPaths[index])); + Require(!ContainsTemporaryFiles(outputDirectory), "Diagnostic export must not leave temporary files."); + + File.Copy(fixturePath, alteredFixturePath); + File.AppendAllText(alteredFixturePath, " "); + bool alteredFixtureRejected = false; + try + { + new LocalG2DiagnosticVisualizationDemo().Export(alteredFixturePath, evidencePath, rejectedFixtureOutputDirectory); + } + catch (Exception) + { + alteredFixtureRejected = true; + } + Require(alteredFixtureRejected, "Fixture bytes that differ from diagnostic evidence must be rejected."); + Require(!Directory.Exists(rejectedFixtureOutputDirectory), + "Fixture hash mismatch must not create an output directory."); + + File.WriteAllText(badEvidencePath, File.ReadAllText(evidencePath).Replace( + "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", + "0000000000000000000000000000000000000000000000000000000000000000")); + bool badEvidenceRejected = false; + try + { + new LocalG2DiagnosticVisualizationDemo().Export(fixturePath, badEvidencePath, rejectedOutputDirectory); + } + catch (Exception) + { + badEvidenceRejected = true; + } + Require(badEvidenceRejected, "Tampered diagnostic evidence must be rejected."); + Require(!Directory.Exists(rejectedOutputDirectory), + "Rejected diagnostic evidence must not create an output directory."); + } + finally + { + if (File.Exists(alteredFixturePath)) File.Delete(alteredFixturePath); + if (Directory.Exists(rejectedFixtureOutputDirectory)) Directory.Delete(rejectedFixtureOutputDirectory, true); + if (File.Exists(badEvidencePath)) File.Delete(badEvidencePath); + if (Directory.Exists(rejectedOutputDirectory)) Directory.Delete(rejectedOutputDirectory, true); + if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); + } + } + + private static void PrintReports(IReadOnlyList scenarios) + { + for (int scenarioIndex = 0; scenarioIndex < scenarios.Count; scenarioIndex++) + { + PathSmoothingComparisonScenarioResult scenario = scenarios[scenarioIndex]; + if (scenario.Comparison == null) + { + Console.WriteLine(scenario.ScenarioId + " coarse=" + scenario.CoarsePathStatus + " " + scenario.Diagnostic); + continue; + } + PrintEntry(scenario.ScenarioId, scenario.Comparison.RawPathBaseline); + for (int entryIndex = 0; entryIndex < scenario.Comparison.Entries.Count; entryIndex++) + PrintEntry(scenario.ScenarioId, scenario.Comparison.Entries[entryIndex]); + Console.WriteLine(scenario.ScenarioId + " report=" + scenario.Report.Status + " " + scenario.Report.Reason); + } + } + + private static void PrintEntry(string scenarioId, PathSmoothingComparisonEntry entry) + { + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "{0} {1} status={2} length={3:F4} peakKappa={4:F4} clearance={5:F4}", + scenarioId, + entry.IsRawPathBaseline ? "RawPath" : entry.Method.ToString(), + entry.Status, + entry.Metrics.PathLengthMeters, + entry.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, + entry.Metrics.MinimumBodyClearanceMeters)); + } + + private static void Verify(string fixturePath) + { + PathSmoothingComparisonRequest request = CreateComparisonRequest(); + PathSmoothingComparisonResult comparison = new PathSmoothingComparisonService().Compare(request); + Require(!comparison.IsCancelled, "Fixture comparison was cancelled."); + Require(request.SmoothingRequest.CoarsePath.Count > 1, "Fixture coarse path is too short."); + + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + var model = new SmoothingFigureModelBuilder().Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + "straight", + "Straight"); + + VerifyObstacleRectanglesAreMerged(fixturePath, comparison, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading)); + VerifyFourFigureDefinitionContract(model); + VerifyNormalLocalG2Series(model); + VerifyPointOnlyRendererContract(model); + + var resolver = new SmoothingFontResolver(); + Require(resolver.TryResolve("SimSun", "Times New Roman", out SmoothingFontResolution fonts, out string fontReason), + "Required report fonts are unavailable: " + fontReason); + byte[] png; + using (fonts) + { + Require(fonts.ChineseFamilyName == "SimSun", "Chinese font must resolve to exact SimSun family."); + Require(fonts.LatinFamilyName == "Times New Roman", "Latin font must resolve to exact Times New Roman family."); + Require(resolver.MeasureMixedText(fonts, "粗路径 κ(s) X (m) −π", 9f).Width > 0f, + "Mixed Chinese/Latin sample must have nonempty measured bounds."); + png = new SmoothingPngRenderer().Render(model, fonts); + } + VerifyPng(png); + + string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-png-" + Guid.NewGuid().ToString("N")); + try + { + var exporter = new SmoothingReportExporter(); + SmoothingReportExportResult report = exporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = outputDirectory, + FileStem = "straight-report", + }); + Require(report.Status == SmoothingReportExportStatus.Success, "Report export failed: " + report.Reason); + Require(report.SvgPaths.Count == 4 && report.PngPaths.Count == 4 && File.Exists(report.CsvPath), + "Successful export must publish four SVGs, four PNGs and one CSV."); + VerifyPublishedFourFigureFiles(report, outputDirectory); + Require(!ContainsTemporaryFiles(outputDirectory), "Successful export must not leave temporary files."); + + string unavailableDirectory = Path.Combine(outputDirectory, "missing-font"); + SmoothingReportExportResult unavailable = exporter.Export(new SmoothingReportExportRequest + { + Model = model, + OutputDirectory = unavailableDirectory, + FileStem = "must-not-write", + ChineseFontFamilyName = "Missing report font", + LatinFontFamilyName = "Times New Roman", + }); + Require(unavailable.Status == SmoothingReportExportStatus.FontUnavailable, + "Missing exact font must report FontUnavailable."); + Require(!Directory.Exists(unavailableDirectory), "Missing-font export must remain atomic and create no output directory."); + } + finally + { + if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); + } + } + + private static PathSmoothingComparisonRequest CreateComparisonRequest() + { + PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(new PlanningMapRequest + { + Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), + ResolutionMm = 50f, + AllowExplicitEmptyMap = true, + }); + Require(mapResult.Succeeded && mapResult.Map != null, "PNG verification map must be created."); + var vehicle = new VehicleParameters + { + LengthMeters = 0.20d, + WidthMeters = 0.20d, + SafetyMarginMeters = 0d, + MaximumCurvaturePerMeter = 100d, + }; + var coarsePath = new List + { + CreatePoint(0.5d, 0.5d, 0d), + CreatePoint(1.0d, 0.5d, 0.5d), + CreatePoint(1.0d, 1.0d, 1.0d), + CreatePoint(1.5d, 1.0d, 1.5d), + }; + var segments = new List + { + new PathSegment(0, TravelDirection.Forward, 0, coarsePath.Count - 1, false, false), + }; + return new PathSmoothingComparisonRequest(new PathSmoothingRequest( + coarsePath, + segments, + mapResult.Map, + vehicle, + new PathSmoothingConfiguration())); + } + + private static void VerifyFourFigureDefinitionContract(SmoothingFigureModel model) + { + SmoothingFigureSet figureSet = new SmoothingFigureSetBuilder().Build(model); + var stems = new List(); + for (int index = 0; index < figureSet.Figures.Count; index++) stems.Add(figureSet.Figures[index].FileStem); + string expected = string.Join(",", new[] + { + "01-coarse-path-overview", + "02-all-paths-comparison", + "03-local-g2-overview", + "04-curvature-comparison", + }); + Require(string.Join(",", stems) == expected, "Four-figure report stems must be stable and ordered."); + Require(!figureSet.Figures[1].ShowsMapContext, "All-path comparison must contain only trajectories, axes and legend."); + Require(figureSet.Figures[2].ShowsMapContext && figureSet.Figures[2].Series[0].Opacity < 1d, + "Local G2 figure must retain a faded raw-path map reference."); + Require(figureSet.Figures[0].WorldScalePointsPerMeter > 0d && + Math.Abs((figureSet.Figures[0].WorldXMaxMeters - figureSet.Figures[0].WorldXMinMeters) / + (figureSet.Figures[0].WorldYMaxMeters - figureSet.Figures[0].WorldYMinMeters) - + figureSet.Figures[0].PlotWidthPoints / figureSet.Figures[0].PlotHeightPoints) < 0.000001d, + "Overhead figures must preserve equal X/Y scale."); + Require(figureSet.Figures[0].LegendYPoints - + (figureSet.Figures[0].PlotYPoints + figureSet.Figures[0].PlotHeightPoints + 31d) >= 8d, + "Legend must leave vertical clearance below the X-axis unit label."); + } + + private static void VerifyNormalLocalG2Series(SmoothingFigureModel model) + { + Require(model.Series.Count == 2, "Normal comparison model must contain only raw-path and Local G2 series."); + SmoothingFigureSeries localG2 = null; + int count = 0; + for (int index = 0; index < model.Series.Count; index++) + { + if (model.Series[index].Key != "local-g2") continue; + localG2 = model.Series[index]; + count++; + } + Require(count == 1, "Normal comparison model must include exactly one Local G2 series."); + Require(Enum.IsDefined(typeof(PathSmoothingStatus), localG2.Status), "Local G2 must retain a defined smoothing status."); + if (localG2.IsCurveVisible) + Require(localG2.Points.Count >= 2, "Visible Local G2 output must include at least two samples."); + } + + private static void VerifyObstacleRectanglesAreMerged(string fixturePath, PathSmoothingComparisonResult comparison, Pose2D start, Pose2D goal) + { + IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); + IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); + int rectangleIndex = -1; + for (int index = 0; index < fixtures.Count; index++) + { + if (fixtures[index].Id == "rectangle-detour") { rectangleIndex = index; break; } + } + Require(rectangleIndex >= 0, "Fixture suite must include rectangle-detour for obstacle rendering verification."); + SmoothingFigureModel model = new SmoothingFigureModelBuilder().Build( + comparison, requests[rectangleIndex].SmoothingRequest.Map, start, goal, "obstacle", "obstacle"); + Require(model.Obstacles.Count > 0, "Rectangle-detour fixture must produce report obstacles."); + for (int firstIndex = 0; firstIndex < model.Obstacles.Count; firstIndex++) + { + SmoothingFigureObstacle first = model.Obstacles[firstIndex]; + for (int secondIndex = firstIndex + 1; secondIndex < model.Obstacles.Count; secondIndex++) + { + SmoothingFigureObstacle second = model.Obstacles[secondIndex]; + bool matchingColumn = Math.Abs(first.X - second.X) < 0.0000001d && Math.Abs(first.Width - second.Width) < 0.0000001d; + bool verticallyAdjacent = Math.Abs((first.Y + first.Height) - second.Y) < 0.0000001d || + Math.Abs((second.Y + second.Height) - first.Y) < 0.0000001d; + Require(!(matchingColumn && verticallyAdjacent), "Adjacent occupied rows must merge into a single obstacle rectangle."); + } + } + } + + private static void VerifyPointOnlyRendererContract(SmoothingFigureModel model) + { + SmoothingFigureDefinition figure = new SmoothingFigureSetBuilder().Build(model).Figures[1]; + string svg = new SmoothingSvgRenderer().Render(figure); + Require(svg.Contains("class=\"trajectory-point\""), "Trajectory samples must render as discrete SVG point markers."); + Require(!svg.Contains("stroke-dasharray") && !svg.Contains("-path\""), "Trajectory SVG output must not use dashed or joined path strokes."); + Require(svg.Contains("X (m)") && svg.Contains("Y (m)"), "Overhead SVG must label metre coordinate axes."); + } + + private static void VerifyDiagnosticPointOverlay(string svg, string pngPath) + { + const string rawHeader = ""; + const string diagnosticHeader = ""; + Require(svg.Contains(rawHeader), "Diagnostic raw series must render as opaque gray outer dots."); + Require(svg.Contains(diagnosticHeader), "Diagnostic candidate must render as opaque red inner dots."); + + IReadOnlyList rawPoints = ReadSvgTrajectoryPoints(svg, "raw"); + IReadOnlyList diagnosticPoints = ReadSvgTrajectoryPoints(svg, "local-g2-diagnostic"); + Require(rawPoints.Count == 88, "Diagnostic SVG must retain the original 88 raw sample centers."); + Require(diagnosticPoints.Count == 88, "Diagnostic SVG must retain the original 88 candidate sample centers."); + for (int index = 0; index < rawPoints.Count; index++) + Require(rawPoints[index].Radius == "2.1", "Diagnostic raw points must use a 2.1 point radius."); + for (int index = 0; index < diagnosticPoints.Count; index++) + Require(diagnosticPoints[index].Radius == "1.35", "Diagnostic candidate points must use a 1.35 point radius."); + + var diagnosticIndicesByCenter = new Dictionary(StringComparer.Ordinal); + for (int index = 0; index < diagnosticPoints.Count; index++) + diagnosticIndicesByCenter[CenterKey(diagnosticPoints[index])] = index; + int sharedCount = 0; + bool hasNonEndpointSharedCenter = false; + for (int index = 0; index < rawPoints.Count; index++) + { + if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(rawPoints[index]), out int diagnosticIndex)) continue; + sharedCount++; + if (index > 0 && index < rawPoints.Count - 1 && diagnosticIndex > 0 && diagnosticIndex < diagnosticPoints.Count - 1) + hasNonEndpointSharedCenter = true; + } + Require(sharedCount == 81 && hasNonEndpointSharedCenter, + "Diagnostic overlay must retain exact shared non-endpoint raw and candidate centers without coordinate offsets."); + VerifyDiagnosticOverlayPixels(pngPath, rawPoints, diagnosticPoints, diagnosticIndicesByCenter); + } + + private static IReadOnlyList ReadSvgTrajectoryPoints(string svg, string seriesKey) + { + string marker = "data-series=\"" + seriesKey + "\""; + int seriesMarker = svg.IndexOf(marker, StringComparison.Ordinal); + Require(seriesMarker >= 0, "Diagnostic SVG series is missing: " + seriesKey); + int seriesStart = svg.LastIndexOf("", seriesMarker, StringComparison.Ordinal); + Require(seriesStart >= 0 && seriesEnd > seriesStart, "Diagnostic SVG series markup is invalid: " + seriesKey); + string series = svg.Substring(seriesStart, seriesEnd - seriesStart); + MatchCollection matches = Regex.Matches(series, + "[^\"]+)\" cy=\"(?[^\"]+)\" r=\"(?[^\"]+)\"/>", + RegexOptions.CultureInvariant); + var points = new List(matches.Count); + for (int index = 0; index < matches.Count; index++) + points.Add(new SvgTrajectoryPoint(matches[index].Groups["x"].Value, matches[index].Groups["y"].Value, matches[index].Groups["r"].Value)); + return points; + } + + private static void VerifyDiagnosticOverlayPixels( + string pngPath, + IReadOnlyList rawPoints, + IReadOnlyList diagnosticPoints, + IReadOnlyDictionary diagnosticIndicesByCenter) + { + const double pixelsPerPoint = 600d / 72d; + const double annulusRadiusPoints = 1.70d; + using (var bitmap = new Bitmap(pngPath)) + { + for (int rawIndex = 1; rawIndex < rawPoints.Count - 1; rawIndex++) + { + SvgTrajectoryPoint raw = rawPoints[rawIndex]; + if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(raw), out int diagnosticIndex) || + diagnosticIndex == 0 || diagnosticIndex == diagnosticPoints.Count - 1) continue; + int centerX = (int)Math.Round(raw.XPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); + int centerY = (int)Math.Round(raw.YPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); + if (!IsDiagnosticRed(bitmap.GetPixel(centerX, centerY))) continue; + + for (int angleIndex = 0; angleIndex < 24; angleIndex++) + { + double angle = 2d * Math.PI * angleIndex / 24d; + int annulusX = (int)Math.Round(centerX + Math.Cos(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); + int annulusY = (int)Math.Round(centerY + Math.Sin(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); + if (annulusX < 0 || annulusX >= bitmap.Width || annulusY < 0 || annulusY >= bitmap.Height) continue; + if (IsRawGray(bitmap.GetPixel(annulusX, annulusY))) return; + } + } + } + throw new InvalidOperationException("A shared non-endpoint sample must have a red center and gray annulus."); + } + + private static bool IsDiagnosticRed(Color color) + { + return color.R >= 140 && color.G <= 95 && color.B <= 100 && color.R >= color.G + 60; + } + + private static bool IsRawGray(Color color) + { + return color.R >= 45 && color.R <= 110 && Math.Abs(color.R - color.G) <= 8 && Math.Abs(color.G - color.B) <= 8; + } + + private static string CenterKey(SvgTrajectoryPoint point) { return point.X + "|" + point.Y; } + + private readonly struct SvgTrajectoryPoint + { + public SvgTrajectoryPoint(string x, string y, string radius) + { + X = x; + Y = y; + Radius = radius; + XPoints = double.Parse(x, CultureInfo.InvariantCulture); + YPoints = double.Parse(y, CultureInfo.InvariantCulture); + } + + public string X { get; } + public string Y { get; } + public string Radius { get; } + public double XPoints { get; } + public double YPoints { get; } + } + + private static void VerifyPublishedFourFigureFiles(SmoothingReportExportResult report, string outputDirectory) + { + var expectedStems = new[] + { + "01-coarse-path-overview", + "02-all-paths-comparison", + "03-local-g2-overview", + "04-curvature-comparison", + }; + VerifyPublishedPaths(report.SvgPaths, expectedStems, ".svg", outputDirectory); + VerifyPublishedPaths(report.PngPaths, expectedStems, ".png", outputDirectory); + Require(!File.Exists(Path.Combine(outputDirectory, "comparison.svg")) && !File.Exists(Path.Combine(outputDirectory, "comparison.png")), + "Four-figure export must not leave legacy composite comparison images."); + } + + private static void VerifyPublishedPaths(IReadOnlyList paths, string[] expectedStems, string extension, string outputDirectory) + { + var actual = new List(); + for (int index = 0; index < paths.Count; index++) actual.Add(paths[index]); + Require(actual.Count == expectedStems.Length, "Four-figure export must publish four " + extension + " files."); + for (int index = 0; index < expectedStems.Length; index++) + { + string expected = Path.Combine(outputDirectory, expectedStems[index] + extension); + Require(actual[index] == expected && File.Exists(actual[index]), "Published " + extension + " path must match the stable figure stem."); + } + } + + private static CoarsePathPoint CreatePoint(double x, double y, double arcLength) + { + return new CoarsePathPoint( + x, y, 0d, 0d, arcLength, TravelDirection.Forward, 0d, 1d, false, CoarsePathPointSource.Start); + } + + private static bool ContainsTemporaryFiles(string directory) + { + foreach (string ignored in Directory.EnumerateFiles(directory, "*.tmp")) return true; + return false; + } + + private static void VerifyPng(byte[] png) + { + Require(png != null && png.Length > PngSignature.Length, "PNG output is empty."); + byte[] data = png ?? throw new InvalidOperationException("PNG output is empty."); + for (int index = 0; index < PngSignature.Length; index++) + Require(data[index] == PngSignature[index], "PNG signature is invalid."); + + bool sawHeader = false; + bool sawPhysicalResolution = false; + int offset = PngSignature.Length; + while (offset < data.Length) + { + Require(offset + 12 <= data.Length, "PNG chunk header is truncated."); + int length = checked((int)ReadUInt32BigEndian(data, offset)); + int dataStart = offset + 8; + int crcStart = checked(dataStart + length); + Require(crcStart + 4 <= data.Length, "PNG chunk data is truncated."); + uint expectedCrc = ReadUInt32BigEndian(data, crcStart); + uint actualCrc = ComputeCrc32(data, offset + 4, length + 4); + Require(expectedCrc == actualCrc, "PNG chunk CRC is invalid."); + + string type = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4); + if (type == "IHDR") + { + Require(length == 13, "IHDR length must be 13."); + Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.WidthPixels, "PNG width must be 4296."); + Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.HeightPixels, "PNG height must be 3120."); + sawHeader = true; + } + else if (type == "pHYs") + { + Require(length == 9, "pHYs length must be 9."); + Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.PixelsPerMeter, + "PNG horizontal density must be 23622 pixels/meter."); + Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.PixelsPerMeter, + "PNG vertical density must be 23622 pixels/meter."); + Require(data[dataStart + 8] == 1, "PNG pHYs unit must be meter."); + sawPhysicalResolution = true; + } + offset = crcStart + 4; + } + Require(sawHeader && sawPhysicalResolution, "PNG must contain IHDR and pHYs chunks."); + } + + private static uint ReadUInt32BigEndian(byte[] data, int offset) + { + return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | + ((uint)data[offset + 2] << 8) | data[offset + 3]; + } + + private static uint ComputeCrc32(byte[] data, int offset, int length) + { + uint crc = 0xffffffffu; + for (int index = 0; index < length; index++) + { + crc ^= data[offset + index]; + for (int bit = 0; bit < 8; bit++) + crc = (crc & 1u) == 0u ? crc >> 1 : (crc >> 1) ^ 0xedb88320u; + } + return crc ^ 0xffffffffu; + } + + private static void Require(bool condition, string message) + { + if (!condition) throw new InvalidOperationException(message); + } +} diff --git a/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 b/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 new file mode 100644 index 0000000..e1e4465 --- /dev/null +++ b/ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 @@ -0,0 +1,28 @@ +param( + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), + [string]$EvidencePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-single-turn') +) + +$ErrorActionPreference = 'Stop' + +$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path +$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) +$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar +if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Report output must stay below $allowedRoot" +} + +$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' +$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path +$resolvedEvidencePath = (Resolve-Path -LiteralPath $EvidencePath).Path + +& dotnet build $projectPath --no-restore +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +& dotnet run --project $hostProject --no-restore -- --export-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath $resolvedOutputDirectory +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Output "Local G2 diagnostic visualization written below $resolvedOutputDirectory" diff --git a/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 b/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 new file mode 100644 index 0000000..2308ccd --- /dev/null +++ b/ClumsyPilot/tests/run_path_smoothing_comparison.ps1 @@ -0,0 +1,32 @@ +param( + [switch]$FixtureOnly, + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports') +) + +$ErrorActionPreference = 'Stop' + +$clumsyPilotRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path +$allowedRoot = [IO.Path]::GetFullPath((Join-Path $clumsyPilotRoot 'obj\path_smoothing_reports')) +$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar +if ($resolvedOutputDirectory -ne $allowedRoot -and -not $resolvedOutputDirectory.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Report output must stay below $allowedRoot" +} + +$projectPath = Join-Path $clumsyPilotRoot 'ClumsyPilot.csproj' +$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path + +& dotnet build $projectPath --no-restore +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +& dotnet run --project $hostProject --no-restore -- --export-fixtures $resolvedFixturePath $resolvedOutputDirectory +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +if (-not $FixtureOnly) { + & dotnet run --project $hostProject --no-restore -- --export-end-to-end $resolvedOutputDirectory + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +Write-Output "Path smoothing reports written below $resolvedOutputDirectory" diff --git a/ClumsyPilot/tests/verify_coarse_path_collision.ps1 b/ClumsyPilot/tests/verify_coarse_path_collision.ps1 new file mode 100644 index 0000000..b7cd515 --- /dev/null +++ b/ClumsyPilot/tests/verify_coarse_path_collision.ps1 @@ -0,0 +1,130 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function New-TestMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $rectangleType = $assembly.GetType($mapping + 'AxisAlignedRectangleObstacle', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) + $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1500, [single]1550, [single]1200, [single]1800)) + $obstacles = [Array]::CreateInstance($obstacleType, 1) + $obstacles.SetValue($obstacle, 0) + $source = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue($source, 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Test map must be created.' + Assert-True $result.Map.PlanningReady 'Test map must be ready.' + return $result.Map +} + +function New-DiagonalCellMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) + $obstacle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) + $obstacles = [Array]::CreateInstance($obstacleType, 1) + $obstacles.SetValue($obstacle, 0) + $source = [Activator]::CreateInstance($manualType, @('diagonal-cell', [long]1, $true, $obstacles)) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue($source, 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Diagonal cell map must be created.' + Assert-True $result.Map.PlanningReady 'Diagonal cell map must be ready.' + Assert-True $result.Map.IsOccupied(30, 30) 'Diagonal cell obstacle must occupy its 50mm cell.' + return $result.Map +} + +$map = New-TestMap +$vehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$vehicle.LengthMeters = 0.20 +$vehicle.WidthMeters = 0.20 +$vehicle.SafetyMarginMeters = 0.0 +$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker + +$diagonalCellMap = New-DiagonalCellMap +$diagonalCellVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$diagonalCellVehicle.LengthMeters = 0.10 +$diagonalCellVehicle.WidthMeters = 0.10 +$diagonalCellVehicle.SafetyMarginMeters = 0.0 +$diagonalCellPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.299, 1.299, 0.0) +$diagonalCellClearance = 0.0 +$diagonalCellSafe = $checker.IsPoseCollisionFree($diagonalCellPose, $diagonalCellMap, $diagonalCellVehicle, 0.20, [ref]$diagonalCellClearance) +Assert-False $diagonalCellSafe 'Expanded diagonal footprint must not bypass an occupied 50mm cell.' + +$edgePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.35, 1.50, 0.0) +$edgeClearance = 0.0 +$edgeSafe = $checker.IsPoseCollisionFree($edgePose, $map, $vehicle, 0.0, [ref]$edgeClearance) +Assert-False $edgeSafe 'Touching an occupied cell must be a collision.' + +$farPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.50, 0.50, 0.0) +$farClearance = 0.0 +$farSafe = $checker.IsPoseCollisionFree($farPose, $map, $vehicle, 0.0, [ref]$farClearance) +Assert-True $farSafe 'Strict distance field clearance must allow the distant pose.' +Assert-True ($farClearance -gt 0.0) 'Distant pose must report positive body clearance.' + +$from = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.20, 1.50, 0.0) +$to = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.80, 1.50, 0.0) +$sweepClearance = 0.0 +$sweepSafe = $checker.IsSweptMotionCollisionFree($from, $to, $map, $vehicle, 0.50, [ref]$sweepClearance) +Assert-False $sweepSafe 'A swept vehicle must not pass through an occupied cell.' + +$gridCenterPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.475, 1.475, 0.0) +$gridCenterClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($gridCenterPose, $map, $vehicle, 0.0, [ref]$gridCenterClearance) 'A pose at an occupied grid center must collide.' + +$subGridPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.481, 1.463, 0.37) +$subGridClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($subGridPose, $map, $vehicle, 0.0, [ref]$subGridClearance) 'An arbitrary sub-grid heading must collide with the thin obstacle.' + +$diagonalPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.32, 1.50, ([Math]::PI / 4.0)) +$diagonalClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($diagonalPose, $map, $vehicle, 0.0, [ref]$diagonalClearance) 'A 45 degree footprint overlap must collide.' + +$outsidePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.05, 0.05, 0.0) +$outsideClearance = 0.0 +Assert-False $checker.IsPoseCollisionFree($outsidePose, $map, $vehicle, 0.0, [ref]$outsideClearance) 'Any footprint corner outside the map must collide.' + +$curveVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters +$curveVehicle.MaximumCurvaturePerMeter = 0.80 +$curveVehicle.MinimumTurningRadiusMeters = 2.0 +$maximumCurvature = 0.0 +$hasMaximumCurvature = [MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.VehicleKinematics]::TryGetMaximumCurvaturePerMeter($curveVehicle, [ref]$maximumCurvature) +Assert-True $hasMaximumCurvature 'Vehicle curvature constraints must be accepted.' +Assert-Near 0.50 $maximumCurvature 'Both curvature constraints must use the conservative minimum.' + +Write-Output 'Coarse path collision checks passed.' diff --git a/ClumsyPilot/tests/verify_coarse_path_search.ps1 b/ClumsyPilot/tests/verify_coarse_path_search.ps1 new file mode 100644 index 0000000..3566c91 --- /dev/null +++ b/ClumsyPilot/tests/verify_coarse_path_search.ps1 @@ -0,0 +1,411 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Null($Actual, [string]$Message) { + if ($null -ne $Actual) { throw $Message } +} + +function Assert-False($Actual, [string]$Message) { + if ($Actual) { throw $Message } +} + +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Throws([scriptblock]$Action, [string]$Message) { + try { + & $Action + } + catch { + return + } + + throw $Message +} + +function Find-Method($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods()) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { + $matches = $false + break + } + } + if ($matches) { return $candidate } + } + return $null +} + +function Find-NonPublicStaticMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Static,NonPublic')) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } + } + if ($matches) { return $candidate } + } + return $null +} + +function New-TestMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.AllowExplicitEmptyMap = $true + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Test map must be created.' + Assert-True $result.Map.PlanningReady 'Test map must be ready.' + return $result.Map +} + +function New-CornerBlockedMap { + $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true) + $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true) + $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true) + $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true) + $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true) + $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true) + $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true) + $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]150, [single]0, [single]150)) + $obstacles = [Array]::CreateInstance($obstacleType, 2) + $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]75, [single]25, [single]0)), 0) + $obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]25, [single]75, [single]0)), 1) + $sources = [Array]::CreateInstance($sourceType, 1) + $sources.SetValue([Activator]::CreateInstance($manualType, @('corner-blocks', [long]1, $true, $obstacles)), 0) + $request = [Activator]::CreateInstance($requestType) + $request.Bounds = $bounds + $request.ResolutionMm = [single]50 + $request.ObstacleSources = $sources + $request.AllowExplicitEmptyMap = $false + $result = [Activator]::CreateInstance($factoryType).Create($request) + Assert-True $result.Succeeded 'Corner-blocked map must be created.' + Assert-True $result.Map.PlanningReady 'Corner-blocked map must be ready.' + return $result.Map +} + +$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$search = $coarsePath + 'Search.' +$primitiveType = $assembly.GetType($search + 'MotionPrimitive', $true) +$generatorType = $assembly.GetType($search + 'MotionPrimitiveGenerator', $true) +$goalCheckerType = $assembly.GetType($search + 'GoalToleranceChecker', $true) + +Assert-True ($primitiveType.GetProperty('ActualLengthMeters') -ne $null) 'MotionPrimitive must expose actual length.' +Assert-True ($primitiveType.GetProperty('Points') -ne $null) 'MotionPrimitive must expose integration points.' +Assert-True ($primitiveType.GetProperty('IsGoalTruncation') -ne $null) 'MotionPrimitive must expose goal truncation state.' + +$poseType = $assembly.GetType($coarsePath + 'Pose2D', $true) +$requestType = $assembly.GetType($coarsePath + 'PlanningRequest', $true) +$configurationType = $assembly.GetType($coarsePath + 'HybridAStarConfiguration', $true) +$vehicleType = $assembly.GetType($coarsePath + 'VehicleParameters', $true) +$directionType = $assembly.GetType($coarsePath + 'TravelDirection', $true) +$goalDirectionType = $assembly.GetType($coarsePath + 'GoalDirectionConstraint', $true) +$generate = Find-Method $generatorType 'Generate' @($poseType, [double], $directionType, $requestType) +Assert-True ($generate -ne $null) 'MotionPrimitiveGenerator must expose Generate(start, curvature, direction, request).' +$isSatisfied = Find-Method $goalCheckerType 'IsSatisfied' @($poseType, $poseType, $configurationType, $directionType, $goalDirectionType) +Assert-True ($isSatisfied -ne $null) 'GoalToleranceChecker must expose the planned goal check.' + +$map = New-TestMap +$vehicle = [Activator]::CreateInstance($vehicleType) +$vehicle.LengthMeters = 0.20 +$vehicle.WidthMeters = 0.20 +$vehicle.SafetyMarginMeters = 0.0 +$vehicle.MaximumCurvaturePerMeter = 1.0 +$config = [Activator]::CreateInstance($configurationType) +$config.PrimitiveLengthMeters = 0.50 +$config.IntegrationStepMeters = 0.05 +$config.MaximumCollisionCheckStepMeters = 0.025 +$config.GoalPositionToleranceMeters = 0.001 +$config.GoalHeadingToleranceRadians = 0.001 +$config.CurvatureLevelCount = 5 +$forward = [Enum]::Parse($directionType, 'Forward') +$reverse = [Enum]::Parse($directionType, 'Reverse') +$anyDirection = [Enum]::Parse($goalDirectionType, 'Any') +$forwardOnly = [Enum]::Parse($goalDirectionType, 'Forward') +$start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) +$generator = [Activator]::CreateInstance($generatorType) + +$request = [Activator]::CreateInstance($requestType) +$request.Map = $map +$request.Vehicle = $vehicle +$request.Configuration = $config +$request.Goal = [Activator]::CreateInstance($poseType, @(3.0, 1.0, 0.0)) +$request.GoalDirection = $anyDirection +$straight = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($straight -ne $null) 'Straight primitive must be generated.' +Assert-Near 0.50 $straight.ActualLengthMeters 'Straight primitive must use the configured cap.' +Assert-Near 1.50 $straight.End.X 'Straight primitive must use analytic integration.' +Assert-Near 1.00 $straight.End.Y 'Straight primitive must not drift laterally.' +Assert-Near 0.00 $straight.End.Heading 'Straight primitive must keep heading.' +Assert-True ($straight.Points.Count -ge 20) 'Point spacing must honor the collision check step.' +$previous = $start +foreach ($point in $straight.Points) { + $distance = [Math]::Sqrt(($point.X - $previous.X) * ($point.X - $previous.X) + ($point.Y - $previous.Y) * ($point.Y - $previous.Y)) + Assert-True ($distance -le 0.025001) 'Point spacing must not exceed the map-safe collision step.' + $previous = $point +} + +$curve = $generate.Invoke($generator, @($start, 1.0, $forward, $request)) +Assert-True ($curve -ne $null) 'Arc primitive must be generated.' +Assert-Near ([Math]::Sin(0.50) + 1.0) $curve.End.X 'Arc X must use analytic integration.' +Assert-Near (1.0 - [Math]::Cos(0.50) + 1.0) $curve.End.Y 'Arc Y must use analytic integration.' +Assert-Near 0.50 $curve.End.Heading 'Arc heading must use signed distance.' + +$reversePrimitive = $generate.Invoke($generator, @($start, 0.0, $reverse, $request)) +Assert-True ($reversePrimitive -ne $null) 'Reverse primitive must be generated.' +Assert-Near 0.50 $reversePrimitive.End.X 'Reverse straight primitive must move behind the vehicle.' + +$request.Goal = [Activator]::CreateInstance($poseType, @(1.30, 1.0, 0.0)) +$truncated = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($truncated -ne $null) 'Goal-truncated primitive must be generated.' +Assert-Near 0.30 $truncated.ActualLengthMeters 'A 0.30m goal must truncate at its first internal point.' +Assert-True $truncated.IsGoalTruncation 'Truncated primitive must record its goal source.' +Assert-Near 1.30 $truncated.End.X 'Truncated primitive must end at the goal point.' + +$request.Goal = $start +$zeroLength = $generate.Invoke($generator, @($start, 0.0, $forward, $request)) +Assert-True ($zeroLength -ne $null) 'Start-at-goal must produce a zero-length candidate.' +Assert-Near 0.0 $zeroLength.ActualLengthMeters 'Start-at-goal candidate must have zero length.' +Assert-True $zeroLength.IsGoalTruncation 'Start-at-goal candidate must have goal truncation source.' + +$wrapPose = [Activator]::CreateInstance($poseType, @(1.0, 1.0, (-[Math]::PI + 0.0005))) +$wrapGoal = [Activator]::CreateInstance($poseType, @(1.0, 1.0, ([Math]::PI - 0.0005))) +$config.GoalHeadingToleranceRadians = 0.002 +Assert-True $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $forward, $forwardOnly)) 'Goal heading tolerance must wrap across pi.' +Assert-False $isSatisfied.Invoke($null, @($wrapPose, $wrapGoal, $config, $reverse, $forwardOnly)) 'Goal direction constraint must reject reverse entry.' + +$levelsMethod = Find-Method $generatorType 'GetCurvatureLevels' @($vehicleType, $configurationType) +Assert-True ($levelsMethod -ne $null) 'MotionPrimitiveGenerator must expose curvature levels.' +$levels = $levelsMethod.Invoke($generator, @($vehicle, $config)) +Assert-True ($levels.Count -eq 5) 'Five configured curvature levels must be generated.' +Assert-Near -1.0 $levels[0] 'First curvature level must be negative maximum.' +Assert-Near 0.0 $levels[2] 'Middle curvature level must be straight.' +Assert-Near 1.0 $levels[4] 'Last curvature level must be positive maximum.' +$adjacent = Find-Method $generatorType 'AreCurvatureLevelsAdjacent' @([int], [int]) +Assert-True ($adjacent -ne $null) 'MotionPrimitiveGenerator must expose curvature adjacency.' +Assert-True $adjacent.Invoke($null, @(2, 3)) 'Neighboring curvature levels must be allowed.' +Assert-False $adjacent.Invoke($null, @(2, 4)) 'Non-neighboring curvature levels must be rejected.' + +$heapGenericType = $assembly.GetType($search + 'BinaryMinHeap`1', $true) +$heapType = $heapGenericType.MakeGenericType([string]) +$heap = [Activator]::CreateInstance($heapType) +$push = Find-Method $heapType 'Push' @([string], [double], [double], [double]) +$pop = Find-Method $heapType 'Pop' @() +Assert-True ($push -ne $null) 'BinaryMinHeap must expose Push(item, f, h, g).' +Assert-True ($pop -ne $null) 'BinaryMinHeap must expose Pop().' +$push.Invoke($heap, @('node-a', 8.0, 2.0, 1.0)) +$push.Invoke($heap, @('node-b', 8.0, 1.0, 1.0)) +$push.Invoke($heap, @('node-c', 8.0, 1.0, 3.0)) +$push.Invoke($heap, @('node-d', 7.0, 9.0, 0.0)) +$push.Invoke($heap, @('node-e', 8.0, 1.0, 3.0)) +Assert-Equal 'node-d' $pop.Invoke($heap, @()) 'Lower F must win.' +Assert-Equal 'node-c' $pop.Invoke($heap, @()) 'Equal F and H must prefer larger G.' +Assert-Equal 'node-e' $pop.Invoke($heap, @()) 'Equal F, H and G must preserve insertion order.' +Assert-Equal 'node-b' $pop.Invoke($heap, @()) 'Equal F must prefer lower H.' +Assert-Equal 'node-a' $pop.Invoke($heap, @()) 'Remaining item must be returned last.' +$firstDeterministicHeap = [Activator]::CreateInstance($heapType) +$secondDeterministicHeap = [Activator]::CreateInstance($heapType) +foreach ($deterministicHeap in @($firstDeterministicHeap, $secondDeterministicHeap)) { + $push.Invoke($deterministicHeap, @('repeat-a', 5.0, 2.0, 1.0)) + $push.Invoke($deterministicHeap, @('repeat-b', 5.0, 1.0, 1.0)) + $push.Invoke($deterministicHeap, @('repeat-c', 5.0, 1.0, 3.0)) +} +$firstDeterministicOrder = @($pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @()), $pop.Invoke($firstDeterministicHeap, @())) -join ',' +$secondDeterministicOrder = @($pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @()), $pop.Invoke($secondDeterministicHeap, @())) -join ',' +Assert-Equal $firstDeterministicOrder $secondDeterministicOrder 'Equivalent heap inputs must have deterministic pop order.' + +$calculatorType = $assembly.GetType($search + 'SearchCostCalculator', $true) +$calculator = [Activator]::CreateInstance($calculatorType) +$calculate = Find-Method $calculatorType 'Calculate' @([double], $directionType, [bool], [double], [double], [int], [double], $configurationType) +Assert-True ($calculate -ne $null) 'SearchCostCalculator must expose the planned equivalent-meter calculation.' +$costConfig = [Activator]::CreateInstance($configurationType) +$cost = $calculate.Invoke($calculator, @(2.0, $reverse, $true, 0.5, 1.0, 2, 0.25, $costConfig)) +Assert-Near 4.55 $cost 'Reverse, gear, curvature and clearance costs must be accumulated.' +$negativeWeightConfig = [Activator]::CreateInstance($configurationType) +$negativeWeightConfig.CurvatureMagnitudeWeight = -0.01 +Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $negativeWeightConfig)) } 'Negative cost weights must be rejected.' +$nonFiniteWeightConfig = [Activator]::CreateInstance($configurationType) +$nonFiniteWeightConfig.ClearanceCostWeight = [double]::NaN +Assert-Throws { $calculate.Invoke($calculator, @(1.0, $forward, $false, 0.0, 1.0, 0, 1.0, $nonFiniteWeightConfig)) } 'Non-finite cost weights must be rejected.' + +$dijkstraType = $assembly.GetType($search + 'GridDijkstraHeuristic', $true) +$dijkstraConstructor = $dijkstraType.GetConstructor(@($map.GetType(), [int], [int])) +Assert-True ($dijkstraConstructor -ne $null) 'GridDijkstraHeuristic must expose the planned grid constructor.' +$getCost = Find-Method $dijkstraType 'GetCost' @([int], [int]) +Assert-True ($getCost -ne $null) 'GridDijkstraHeuristic must expose GetCost(row, col).' +$openHeuristic = $dijkstraConstructor.Invoke(@($map, 2, 2)) +$expectedDiagonalCost = 2.0 * [Math]::Sqrt(2.0) * $map.ResolutionMeters +Assert-Near $expectedDiagonalCost $getCost.Invoke($openHeuristic, @(0, 0)) 'Open-grid diagonal movement must cost sqrt(2) per cell.' +$cornerBlockedMap = New-CornerBlockedMap +$blockedHeuristic = $dijkstraConstructor.Invoke(@($cornerBlockedMap, 1, 1)) +Assert-True ([double]::IsPositiveInfinity($getCost.Invoke($blockedHeuristic, @(0, 0)))) 'Diagonal corner cutting through two occupied orthogonal cells must be forbidden.' + +$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $true) +$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $true) +$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, + @([Threading.CancellationToken], [TimeSpan]), $null) +Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' +$tryCreateDijkstra = Find-NonPublicStaticMethod $dijkstraType 'TryCreate' @($map.GetType(), [int], [int], $operationBudgetType, + $dijkstraType.MakeByRefType(), $operationStopReasonType.MakeByRefType()) +Assert-True ($tryCreateDijkstra -ne $null) 'GridDijkstraHeuristic must expose an internal budget-aware TryCreate method.' +$expiredBudget = $budgetConstructor.Invoke(@([Threading.CancellationToken]::None, [TimeSpan]::Zero)) +$dijkstraArguments = [object[]]@($map, 2, 2, $expiredBudget, $null, $null) +$dijkstraCreated = $tryCreateDijkstra.Invoke($null, $dijkstraArguments) +Assert-False $dijkstraCreated 'An expired total budget must stop Dijkstra construction.' +Assert-Null $dijkstraArguments[4] 'Stopped Dijkstra construction must not publish a partial heuristic.' +Assert-Equal 'TimedOut' $dijkstraArguments[5].ToString() 'Expired Dijkstra construction must report timeout.' + +$largeBounds = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true), + @([single]0, [single]40000, [single]0, [single]20000)) +$largeMapRequest = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest', $true)) +$largeMapRequest.Bounds = $largeBounds +$largeMapRequest.ResolutionMm = [single]20 +$largeMapRequest.AllowExplicitEmptyMap = $true +$largeMap = [Activator]::CreateInstance($assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory', $true)).Create($largeMapRequest) +Assert-True $largeMap.Succeeded 'Large empty map for Dijkstra cancellation must be created before cancellation starts.' +$midDijkstraCancellation = New-Object Threading.CancellationTokenSource +$midDijkstraBudget = $budgetConstructor.Invoke(@($midDijkstraCancellation.Token, [TimeSpan]::FromSeconds(2))) +$midDijkstraCancellation.CancelAfter(1) +$midDijkstraTimer = [Diagnostics.Stopwatch]::StartNew() +$midDijkstraArguments = [object[]]@($largeMap.Map, 500, 1000, $midDijkstraBudget, $null, $null) +$midDijkstraCreated = $tryCreateDijkstra.Invoke($null, $midDijkstraArguments) +$midDijkstraTimer.Stop() +Assert-False $midDijkstraCreated 'Cancellation during Dijkstra initialization must stop construction.' +Assert-Null $midDijkstraArguments[4] 'Mid-Dijkstra cancellation must not publish a partial heuristic.' +Assert-Equal 'Cancelled' $midDijkstraArguments[5].ToString() 'Mid-Dijkstra cancellation must retain cancellation status.' +Assert-True ($midDijkstraTimer.Elapsed -lt [TimeSpan]::FromSeconds(2)) 'Dijkstra cancellation must return within the configured response bound.' + +Write-Output 'Coarse path search primitive checks passed.' + +$nodeType = $assembly.GetType($search + 'HybridAStarNode', $true) +$nodeKeyType = $assembly.GetType($search + 'HybridAStarNodeKey', $true) +$searchType = $assembly.GetType($search + 'HybridAStarSearch', $true) +$searchResultType = $assembly.GetType($search + 'HybridAStarSearchResult', $true) +$searchMethod = Find-Method $searchType 'Search' @($requestType, [Threading.CancellationToken]) +Assert-True ($searchMethod -ne $null) 'HybridAStarSearch must expose Search(request, cancellationToken).' +Assert-True ($searchResultType.GetProperty('Status') -ne $null) 'Search result must expose planning status.' +Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) 'Search result must expose its original termination reason.' +Assert-True ($searchResultType.GetProperty('SuccessNodeIndex') -ne $null) 'Search result must expose the successful node index.' +Assert-True ($searchResultType.GetProperty('ReopenedNodeCount') -ne $null) 'Search result must expose reopened node count.' +Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entry count.' +Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak count.' +Assert-True ($nodeType.GetProperty('ParentNodeIndex') -ne $null) 'Search node must expose its parent node index.' +Assert-True ($nodeKeyType.GetProperty('HeadingIndex') -ne $null) 'Search node key must contain a heading index.' +Assert-True ($nodeKeyType.GetProperty('Direction') -ne $null) 'Search node key must contain a travel direction.' +Assert-True ($nodeKeyType.GetProperty('CurvatureLevelIndex') -ne $null) 'Search node key must contain a curvature level index.' + +$goalCandidateProperty = $nodeType.GetProperty('IsGoalCandidate') +Assert-True ($goalCandidateProperty -ne $null) 'Search node must mark a goal-truncated candidate explicitly.' +$nodeConstructor = $nodeType.GetConstructor(@([int], $nodeKeyType, $poseType, [int], $primitiveType, [double], [double], [double], [bool])) +Assert-True ($nodeConstructor -ne $null) 'Search node must accept the explicit goal-candidate marker.' +$admissionMethod = $searchType.GetMethod('ShouldEnqueueSuccessor', [Reflection.BindingFlags]'NonPublic,Static') +Assert-True ($admissionMethod -ne $null) 'Search must expose its private successor admission policy for reflection regression coverage.' + +$candidateStart = [Activator]::CreateInstance($poseType, @(1.000, 1.000, 0.0)) +$candidateGoal = [Activator]::CreateInstance($poseType, @(1.024, 1.000, 0.0)) +$candidatePoints = [Array]::CreateInstance($poseType, 1) +$candidatePoints.SetValue($candidateGoal, 0) +$candidatePrimitive = [Activator]::CreateInstance($primitiveType, @($candidateStart, $forward, 0.0, 0.024, $candidatePoints, [double[]]@(1.0), $true)) +$sharedKey = [Activator]::CreateInstance($nodeKeyType, @(20, 20, 0, $forward, 2)) +$normalNode = $nodeConstructor.Invoke(@(10, $sharedKey, $candidateStart, 0, $null, 0.0, 0.250, 0.0, $false)) +$goalCandidateNode = $nodeConstructor.Invoke(@(11, $sharedKey, $candidateGoal, 10, $candidatePrimitive, 0.0, 0.300, 0.0, $true)) +$dominanceConfiguration = [Activator]::CreateInstance($configurationType) +$dominanceConfiguration.GoalPositionToleranceMeters = 0.001 +$dominanceConfiguration.GoalHeadingToleranceRadians = 0.001 +Assert-True $candidatePrimitive.IsGoalTruncation 'Regression setup must use a goal-truncated motion primitive.' +Assert-True $normalNode.Key.Equals($goalCandidateNode.Key) 'Regression setup must share one discrete state key.' +Assert-False $isSatisfied.Invoke($null, @($normalNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Lower-G normal node must remain outside the continuous goal tolerance.' +Assert-True $isSatisfied.Invoke($null, @($goalCandidateNode.Pose, $candidateGoal, $dominanceConfiguration, $forward, $forwardOnly)) 'Goal candidate must satisfy the continuous goal tolerance.' +Assert-False $admissionMethod.Invoke($null, @($normalNode, 0.250)) 'Equal-cost normal state must be dominated by the best same-key label.' +Assert-True $admissionMethod.Invoke($null, @($goalCandidateNode, 0.250)) 'Goal candidate must enter Open List even when a lower-G normal state has the same key.' + +function New-SearchRequest([object]$Goal, [object]$GoalDirection, [bool]$AllowReverse) { + $searchRequest = [Activator]::CreateInstance($requestType) + $searchRequest.Map = $map + $searchRequest.Start = [Activator]::CreateInstance($poseType, @(1.0, 1.0, 0.0)) + $searchRequest.Goal = $Goal + $searchRequest.Vehicle = $vehicle + $searchConfiguration = [Activator]::CreateInstance($configurationType) + $searchConfiguration.PrimitiveLengthMeters = 0.50 + $searchConfiguration.IntegrationStepMeters = 0.05 + $searchConfiguration.MaximumCollisionCheckStepMeters = 0.025 + $searchConfiguration.GoalPositionToleranceMeters = 0.001 + $searchConfiguration.GoalHeadingToleranceRadians = 0.001 + $searchConfiguration.CurvatureLevelCount = 5 + $searchConfiguration.MaximumExpandedNodes = 10000 + $searchConfiguration.SearchTimeout = [TimeSpan]::FromSeconds(2.0) + $searchConfiguration.AllowReverse = $AllowReverse + $searchRequest.Configuration = $searchConfiguration + $searchRequest.GoalDirection = $GoalDirection + return $searchRequest +} + +$searcher = [Activator]::CreateInstance($searchType) +$searchGoal = [Activator]::CreateInstance($poseType, @(1.20, 1.0, 0.0)) +$searchRequest = New-SearchRequest $searchGoal $forwardOnly $false +$searchResult = $searchMethod.Invoke($searcher, @($searchRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $searchResult.Status.ToString() 'Empty map forward search must succeed.' +Assert-True ($null -ne $searchResult.SuccessNodeIndex) 'Success must be reported only with a popped goal candidate node.' +Assert-True ($searchResult.ExpandedNodeCount -gt 0) 'A successful search must expand the selected goal candidate.' + +$atGoal = New-SearchRequest $searchRequest.Start $forwardOnly $false +$atGoalResult = $searchMethod.Invoke($searcher, @($atGoal, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $atGoalResult.Status.ToString() 'A zero-length goal candidate must enter and leave the open list successfully.' +Assert-True ($null -ne $atGoalResult.SuccessNodeIndex) 'A zero-length goal candidate must have a node index.' + +$cancelledRequest = New-SearchRequest $searchGoal $forwardOnly $false +$cancellationSource = New-Object Threading.CancellationTokenSource +$cancellationSource.Cancel() +$cancelledResult = $searchMethod.Invoke($searcher, @($cancelledRequest, $cancellationSource.Token)) +Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Cancellation must be observed before node expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) 'Cancelled search must retain a non-empty reason.' + +$limitedRequest = New-SearchRequest $searchGoal $forwardOnly $false +$limitedRequest.Configuration.MaximumExpandedNodes = 0 +$limitedResult = $searchMethod.Invoke($searcher, @($limitedRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchNodeLimitExceeded' $limitedResult.Status.ToString() 'Node limit must be checked before expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) 'Node-limited search must retain a non-empty reason.' + +$timedOutRequest = New-SearchRequest $searchGoal $forwardOnly $false +$timedOutRequest.Configuration.SearchTimeout = [TimeSpan]::Zero +$timedOutResult = $searchMethod.Invoke($searcher, @($timedOutRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchTimeout' $timedOutResult.Status.ToString() 'Timeout must be checked before expansion.' +Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) 'Timed-out search must retain a non-empty reason.' +Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) 'Cancelled and node-limited searches must retain different reasons.' +Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) 'Node-limited and timed-out searches must retain different reasons.' + +$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | + Where-Object { + $_.Name -eq 'Search' -and + $_.GetParameters().Length -eq 2 -and + $_.GetParameters()[1].ParameterType -eq $operationBudgetType + } | + Select-Object -First 1 +Assert-True ($internalSearchMethod -ne $null) 'Search must retain its internal shared-budget overload.' +$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) +Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() 'A missing internal budget must be mapped to InternalError.' +Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) 'Internal search errors must retain the exception type.' + +Write-Output 'Coarse path Hybrid A star search checks passed.' diff --git a/ClumsyPilot/tests/verify_coarse_path_ui.ps1 b/ClumsyPilot/tests/verify_coarse_path_ui.ps1 new file mode 100644 index 0000000..58b2d00 --- /dev/null +++ b/ClumsyPilot/tests/verify_coarse_path_ui.ps1 @@ -0,0 +1,122 @@ +$ErrorActionPreference = 'Stop' + +function Assert-True([bool]$condition, [string]$message) { + if (-not $condition) { throw $message } +} + +function Assert-Match([string]$content, [string]$pattern, [string]$message) { + Assert-True ($content -match $pattern) $message +} + +function Assert-NotMatch([string]$content, [string]$pattern, [string]$message) { + Assert-True ($content -notmatch $pattern) $message +} + +$sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\Test\MovementTest.CoarsePathTest.cs' +Assert-True (Test-Path -LiteralPath $sourcePath) 'P1 coarse-path UI movement-test source must exist.' +$source = Get-Content -Raw -Encoding UTF8 $sourcePath + +Assert-Match $source 'class\s+CoarsePathPlanningTest\s*:\s*MovementTest' 'The UI entry must inherit MovementTest.' +Assert-Match $source 'namespace\s+MultiWheelC\s*;' 'MovementTest entries must use the project discovery namespace.' +Assert-Match $source 'CoarsePathPlanningService' 'The UI must keep one shared planning service for map-cache reuse.' +Assert-Match $source 'Task\.Run\s*\(' 'Planning must run in a background Task.' +Assert-Match $source 'CancellationTokenSource' 'The UI must retain a cancellation source for its active task.' +Assert-Match $source 'override\s+void\s+TestStop\s*\(' 'The UI must implement TestStop cancellation.' +Assert-Match $source 'CoarsePathScenarioFactory\.Create\s*\(' 'The UI must expose fixed scenario entries through the factory.' +Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' +Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' +Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' +Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' +Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' +Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' +Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' +Assert-Match $source 'UI\.GetInput\s*\(' 'The manual demo must accept user input.' +Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' 'The manual UI must read a finite positive timeout.' +Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' 'The manual UI must apply the timeout to the current job.' +Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' 'The manual timeout must convert seconds to TimeSpan.' +Assert-Match $source 'timeoutSeconds\s*<=\s*0' 'The manual timeout must reject zero and negative values.' +Assert-Match $source 'getCartLocation\s*\(' 'The manual demo must read the AMR body-center pose.' +Assert-Match $source 'DrawLegend\s*\(' 'The painter output must include a visual legend.' +Assert-Match $source 'PlanningGridMap' 'The painter output must consume the planning grid result.' +Assert-Match $source '\.IsOccupied\s*\(' 'The painter output must draw occupied grid cells.' +Assert-Match $source 'ResolutionMm' 'The painter output must draw the grid at its true resolution.' +Assert-Match $source 'SnapshotId' 'The painter output must show the map snapshot identity.' +Assert-Match $source 'IsGearSwitchPoint' 'The painter output must highlight gear-switch points.' +Assert-Match $source 'GoalPositionToleranceMeters' 'The painter output must draw goal tolerance.' +Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) 'The status layer and Toast must both show path-search elapsed time.' +Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' 'The failure Toast must include the termination reason.' +Assert-Match $source 'ExpandedNodeCount' 'The status layer must show expanded-node statistics.' +Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' 'The status layer must show the effective turning radius.' +Assert-NotMatch $source 'PlanningMapFactory' 'Movement tests must not build maps directly; use CoarsePathPlanningService.' +Assert-NotMatch $source '\.Result\b' 'The UI must not block on Task.Result.' +Assert-NotMatch $source '\.Wait\s*\(' 'The UI must not block the movement-test thread.' + +$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') +Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' +$runnerSource = $source.Substring($runnerStart) +Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' +Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' +Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' +Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' +Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' +Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' +$inputFailureStart = $runnerSource.IndexOf('internal static void ShowInputFailure') +$inputFailureEnd = $runnerSource.IndexOf('private static void Finish', $inputFailureStart) +Assert-True ($inputFailureStart -ge 0 -and $inputFailureEnd -gt $inputFailureStart) 'Input failure renderer must be isolated before completion handling.' +$inputFailureSource = $runnerSource.Substring($inputFailureStart, $inputFailureEnd - $inputFailureStart) +Assert-Match $inputFailureSource 'Painter\.DrawText\s*\(' 'Invalid AMR input must be shown on the Painter status layer.' + +$readmePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\CoarsePath\README.md' +Assert-True (Test-Path -LiteralPath $readmePath) 'CoarsePath README must exist beside its module.' +$readme = Get-Content -Raw -Encoding UTF8 $readmePath +foreach ($requiredText in @( + 'CoarsePathPlanningV1', + 'CoarsePathPlanningTest', + 'CreateManualGoalDemo', + 'getCartLocation', + 'mm', + 'deg', + 'rad', + 'CancellationTokenSource', + 'TestStop', + 'NoFeasiblePath', + 'IsGearSwitchPoint')) { + Assert-True ($readme.Contains($requiredText)) "P1 README must document $requiredText." +} + +$readmeStructure = @( + 'File Structure', + 'Planning Data Flow', + 'Build Status and Stop', + 'Coordinates and Units', + 'Minimal Call Example', + 'Cache and SourceVersion', + 'Detailed Usage Guide', + 'P1 Manual Tests and Visualization', + 'Common Errors', + 'First-Version Limits', + 'CoarsePathPlanningService.Plan(job, cancellationToken)', + 'CoarsePathPlanningJob', + 'PlanningGridMap', + 'SourceVersion', + 'CoarsePathPlanningV1', + 'CancellationTokenSource', + 'NoFeasiblePath', + 'IsGearSwitchPoint', + 'CreateManualObstacleDemo', + 'ManualCoarsePathObstacle', + 'manual-user-input', + '0-20', + 'PathSearchElapsed', + '1.20 m', + 'Reeds-Shepp', + 'AxisAlignedRectangle', + 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', + 'DetectionHeadingRadians', + '../Map/README.md' +) +foreach ($requiredText in $readmeStructure) { + Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." +} + +Write-Output 'Coarse path P1 UI source checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 b/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 new file mode 100644 index 0000000..350f5a4 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_documentation.ps1 @@ -0,0 +1,68 @@ +$ErrorActionPreference = 'Stop' + +$root = Join-Path $PSScriptRoot '..' +$readmePath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\README.md' +$demoPath = Join-Path $root 'ParkrobTrajplanner\PathSmoothing\Test\PathSmoothingComparisonDemo.cs' +$runnerPath = Join-Path $PSScriptRoot 'run_path_smoothing_comparison.ps1' + +function Assert-True($actual, [string]$message) { + if (-not $actual) { throw $message } +} + +Assert-True (Test-Path -LiteralPath $readmePath) 'Path smoothing README is missing.' +Assert-True (Test-Path -LiteralPath $demoPath) 'Path smoothing comparison demo is missing.' +Assert-True (Test-Path -LiteralPath $runnerPath) 'Path smoothing comparison batch runner is missing.' + +$readme = Get-Content -Raw -Encoding UTF8 $readmePath +foreach ($section in @( + 'Module Overview', + 'File Structure', + 'Smoothing Data Flow', + 'Result Status and Publication Rules', + 'Coordinates and Units', + 'Minimal Call Example', + 'Detailed Usage Guide', + 'Fixture Reports and Visualization', + 'Common Errors', + 'First-Version Limits')) { + Assert-True $readme.Contains($section) "README must contain section: $section" +} +foreach ($requiredText in @( + 'Local G2', + 'MinimumClearanceReserveMeters', + 'PathSmoothingService', + 'Complete', + 'PartialImprovement', + 'NotNeeded', + 'Unchanged', + 'SimSun', + 'Times New Roman', + '01-coarse-path-overview', + '02-all-paths-comparison', + '03-local-g2-overview', + '04-curvature-comparison', + '05-local-g2-diagnostic-candidate')) { + Assert-True $readme.Contains($requiredText) "README must document: $requiredText" +} + +$demo = Get-Content -Raw -Encoding UTF8 $demoPath +foreach ($requiredText in @( + 'PathSmoothingComparisonDemo', + 'PathSmoothingService', + 'PathSmoothingComparisonService', + 'SmoothingReportExporter', + 'SmoothingScenarioFactory', + 'PathSmoothingStatus.Complete', + 'PathSmoothingStatus.PartialImprovement', + 'PathSmoothingStatus.NotNeeded', + 'PathSmoothingStatus.Unchanged')) { + Assert-True $demo.Contains($requiredText) "Demo must use: $requiredText" +} + +$runner = Get-Content -Raw -Encoding UTF8 $runnerPath +Assert-True ($runner -match '\[switch\]\$FixtureOnly') 'Batch runner must offer -FixtureOnly.' +Assert-True $runner.Contains('obj\path_smoothing_reports') 'Batch runner must write reports below ClumsyPilot/obj/path_smoothing_reports.' +Assert-True $runner.Contains('--export-fixtures') 'Batch runner must export all eight fast fixtures by default.' +Assert-True ($runner -notmatch 'ParkrobTrajplanner\\PathSmoothing\\.*\.(svg|png|csv)') 'Batch runner must never write report files below source directories.' + +Write-Output 'Path smoothing LocalG2 documentation checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 b/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 index ec883c4..44bcd27 100644 --- a/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 +++ b/ClumsyPilot/tests/verify_path_smoothing_fixtures.ps1 @@ -26,7 +26,7 @@ function Assert-ThrowsMatching([scriptblock]$Action, [string]$ExpectedPattern, [ function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } $root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.' -$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison.' +$comparison = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison.' $loaderType = Get-RequiredType ($root + 'SmoothingScenarioFixtureLoader') $factoryType = Get-RequiredType ($root + 'SmoothingScenarioFactory') $fixtureType = Get-RequiredType ($root + 'SmoothingScenarioFixture') diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 index 86caacd..f75ebb8 100644 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1 @@ -197,16 +197,50 @@ $realCandidates = (Get-InternalMethod $builderType 'Build').Invoke($realBuilder, Assert-True ($realCandidates.Count -gt 0) 'SingleTurn must build real Local G2 candidates.' $realEvaluator = New-InternalInstance $evaluatorType $duplicateFailures = @() +$acceptedSplitCandidates = @() +$maximumAnchorError = 0.0 +$maximumConnectionPositionError = 0.0 +$maximumTangentDirectionError = 0.0 +$maximumCurvatureError = 0.0 foreach ($realCandidate in $realCandidates) { $evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @( - $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, [Threading.CancellationToken]::None)) + $preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options, + [Threading.CancellationToken]::None)) $failureReason = Get-InternalProperty $evaluation 'FailureReason' - if ($failureReason -eq 2) { - $candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex' - $duplicateFailures += $candidateIndex + if ($failureReason -eq 2) { $duplicateFailures += (Get-InternalProperty $realCandidate 'CandidateIndex') } + + $hasDifferentScale = $false + foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) { + $anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters') + $maximumAnchorError = [Math]::Max($maximumAnchorError, $anchorError) + $maximumConnectionPositionError = [Math]::Max( + $maximumConnectionPositionError, + [double](Get-InternalProperty $diagnostic 'ConnectionPositionErrorMeters')) + $maximumTangentDirectionError = [Math]::Max( + $maximumTangentDirectionError, + [double](Get-InternalProperty $diagnostic 'TangentDirectionErrorRadians')) + $maximumCurvatureError = [Math]::Max( + $maximumCurvatureError, + [double](Get-InternalProperty $diagnostic 'CurvatureErrorPerMeter')) + $incoming = [double](Get-InternalProperty $diagnostic 'IncomingDerivativeScale') + $outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale') + if ([Math]::Abs($incoming - $outgoing) -gt 1e-10) { $hasDifferentScale = $true } + } + if ((Get-InternalProperty $evaluation 'Accepted') -and $hasDifferentScale) { + $acceptedSplitCandidates += $realCandidate } } Assert-Equal 0 $duplicateFailures.Count ` - 'SingleTurn builder candidates must not fail evaluator window analysis due to duplicate or degenerate points.' + 'SingleTurn candidates must not fail raw-window analysis on coincident boundary points.' +Assert-True ($maximumAnchorError -le 1e-9) ` + 'Every internal primitive boundary must remain at its original coordinate.' +Assert-True ($maximumConnectionPositionError -le 1e-9) ` + 'Every real split-scale connection must be position continuous.' +Assert-True ($maximumTangentDirectionError -le 1e-8) ` + 'Every real split-scale connection must preserve unit tangent direction.' +Assert-True ($maximumCurvatureError -le 1e-8) ` + 'Every real split-scale connection must preserve geometric curvature.' +Assert-True ($acceptedSplitCandidates.Count -gt 0) ` + 'SingleTurn must accept a zero-coordinate-offset candidate with different incoming/outgoing scales.' Write-Output 'Path smoothing Local G2 candidate checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 new file mode 100644 index 0000000..3fcb115 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_contract.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = 'Stop' + +$root = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing' +$sourcePath = Join-Path $root 'Contracts\SmoothedPathPointSource.cs' +$evidencePath = Join-Path $root 'Test\Fixtures\local-g2-diagnostic-single-turn.json' + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +$source = Get-Content -Raw -Encoding UTF8 $sourcePath +Assert-True ($source -match 'LocalG2Transition\s*=\s*4') ` + 'LocalG2Transition must retain serialized value 4 for diagnostic evidence compatibility.' + +$evidence = Get-Content -Raw -Encoding UTF8 $evidencePath | ConvertFrom-Json +foreach ($point in $evidence.CandidatePoints) { + Assert-True ($point.Source -eq 4) 'Diagnostic evidence points must use the LocalG2Transition serialized value.' +} + +Write-Output 'Local G2 diagnostic serialization contract checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 new file mode 100644 index 0000000..3bf80b6 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 @@ -0,0 +1,11 @@ +param( + [string]$FixturePath = (Join-Path $PSScriptRoot "..\\ParkrobTrajplanner\\PathSmoothing\\Test\\Fixtures\\path-smoothing-fixtures.json"), + [string]$EvidencePath = (Join-Path $PSScriptRoot "..\\ParkrobTrajplanner\\PathSmoothing\\Test\\Fixtures\\local-g2-diagnostic-single-turn.json") +) + +$hostProject = Join-Path $PSScriptRoot "PathSmoothingPngVerificationHost\\PathSmoothingPngVerificationHost.csproj" +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path +$resolvedEvidencePath = (Resolve-Path -LiteralPath $EvidencePath).Path + +& dotnet run --project $hostProject --no-restore -- --verify-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 new file mode 100644 index 0000000..d4e7c75 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 @@ -0,0 +1,98 @@ +param( + [string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'), + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') +) + +$ErrorActionPreference = 'Stop' +$newtonsoft = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll' +if (Test-Path $newtonsoft) { $null = [Reflection.Assembly]::LoadFrom($newtonsoft) } +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} +function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { + throw "$Message Expected=$Expected Actual=$Actual" + } +} +function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$facade = $root + 'Facade.' +$test = $root + 'Test.' +$serviceType = Get-RequiredType ($facade + 'PathSmoothingService') +$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$factoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory') +$service = [Activator]::CreateInstance($serviceType) +$smooth = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken])) +$createFixtures = $factoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])) +Assert-True ($null -ne $smooth) 'Local G2 must use the public service entrypoint.' + +$publishedStatuses = @('Complete', 'PartialImprovement', 'NotNeeded', 'Unchanged') + +function New-LocalG2Request($BaseRequest, [scriptblock]$Configure = $null) { + $configuration = [Activator]::CreateInstance($configurationType) + if ($null -ne $Configure) { & $Configure $configuration } + return [Activator]::CreateInstance($requestType, @( + $BaseRequest.CoarsePath, $BaseRequest.Segments, $BaseRequest.Map, + $BaseRequest.Vehicle, $configuration)) +} + +function Invoke-LocalG2($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) { + return $smooth.Invoke($service, @($Request, $CancellationToken)) +} + +function Assert-PublishedPath($Result, [string]$Name) { + Assert-True ($Result.Path.Count -gt 0) "$Name must publish a complete path." + Assert-Equal 0 $Result.Segments[0].StartIndex "$Name segments must start at zero." + Assert-Equal ($Result.Path.Count - 1) $Result.Segments[-1].EndIndex "$Name segments must cover the path." + Assert-True $Result.Diagnostics.Metrics.IsFeasible "$Name diagnostics must be feasible." + Assert-True ($Result.Diagnostics.Metrics.MinimumBodyClearanceMeters -ge 0.0) "$Name must remain collision-free at the configured 0 m reserve." +} + +function Assert-Equivalent($First, $Second, [string]$Name) { + Assert-Equal $First.Status $Second.Status "$Name must retain deterministic status." + Assert-Equal $First.Path.Count $Second.Path.Count "$Name must retain deterministic point count." + Assert-Equal $First.RegionReports.Count $Second.RegionReports.Count "$Name must retain deterministic report count." + for ($index = 0; $index -lt $First.Path.Count; $index++) { + Assert-Near $First.Path[$index].X $Second.Path[$index].X "$Name point $index must retain X." + Assert-Near $First.Path[$index].Y $Second.Path[$index].Y "$Name point $index must retain Y." + } + for ($index = 0; $index -lt $First.RegionReports.Count; $index++) { + Assert-Equal $First.RegionReports[$index].SelectedCandidateIndex $Second.RegionReports[$index].SelectedCandidateIndex "$Name report $index must retain candidate selection." + Assert-Equal $First.RegionReports[$index].Status $Second.RegionReports[$index].Status "$Name report $index must retain status." + } +} + +$fixtures = $createFixtures.Invoke($null, @((Resolve-Path $FixturePath).Path)) +for ($index = 0; $index -lt $fixtures.Count; $index++) { + $request = New-LocalG2Request $fixtures[$index].SmoothingRequest + $first = Invoke-LocalG2 $request + $second = Invoke-LocalG2 $request + Assert-Equivalent $first $second "Fixture $index" + $status = $first.Status.ToString() + Assert-True ($status -in @('Complete', 'PartialImprovement', 'NotNeeded', 'Unchanged', 'Failed', 'InvalidInput', 'Infeasible')) "Fixture $index must return a defined Local G2 result status." + if ($publishedStatuses -contains $status) { + Assert-PublishedPath $first "Fixture $index" + } + else { + Assert-Equal 0 $first.Path.Count "Unpublished fixture $index must not expose a path." + Assert-Equal 0 $first.Segments.Count "Unpublished fixture $index must not expose segments." + } +} + +$cancelSource = [Threading.CancellationTokenSource]::new() +try { + $cancelSource.Cancel() + $cancelled = Invoke-LocalG2 (New-LocalG2Request $fixtures[1].SmoothingRequest) $cancelSource.Token + Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancellation must propagate through Local G2.' + Assert-Equal 0 $cancelled.Path.Count 'A cancelled run must not publish a partial path.' +} +finally { + $cancelSource.Dispose() +} + +Write-Output 'Local G2 service integration checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 new file mode 100644 index 0000000..ee829cd --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_only_layout.ps1 @@ -0,0 +1,75 @@ +param( + [string]$PathSmoothingRoot = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing') +) + +$ErrorActionPreference = 'Stop' + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-FileExists([string]$RelativePath) { + Assert-True (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath) -PathType Leaf) "Missing required file: $RelativePath" +} + +function Assert-DirectoryExists([string]$RelativePath) { + Assert-True (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath) -PathType Container) "Missing required directory: $RelativePath" +} + +function Assert-PathAbsent([string]$RelativePath) { + Assert-True (-not (Test-Path -LiteralPath (Join-Path $PathSmoothingRoot $RelativePath))) "Removed legacy path still exists: $RelativePath" +} + +Assert-DirectoryExists 'Contracts' +Assert-DirectoryExists 'Facade' +Assert-DirectoryExists 'LocalG2' +Assert-DirectoryExists 'Processing' +Assert-DirectoryExists 'Validation' +Assert-DirectoryExists 'Output' +Assert-DirectoryExists 'Output\Comparison' +Assert-DirectoryExists 'Output\Visualization' +Assert-DirectoryExists 'Test' +Assert-FileExists 'README.md' + +foreach ($legacyPath in @( + 'Algorithms', + 'Comparison', + 'Visualization', + 'Contracts\CubicBSplineOptions.cs', + 'Contracts\LocalCubicBezierOptions.cs', + 'Contracts\PiecewiseQuinticOptions.cs', + 'Output\Comparison\SmoothingMethodRanker.cs' +)) { + Assert-PathAbsent $legacyPath +} + +$methodSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'Contracts\SmoothingMethod.cs') +Assert-True ($methodSource -match 'LocalG2Quintic') 'SmoothingMethod must retain LocalG2Quintic.' +foreach ($legacyMethod in @('CubicBSpline', 'LocalCubicBezier', 'PiecewiseQuintic')) { + Assert-True (-not $methodSource.Contains($legacyMethod)) "SmoothingMethod must not retain $legacyMethod." +} + +$configurationSource = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'Contracts\PathSmoothingConfiguration.cs') +foreach ($legacyConfiguration in @('CubicBSpline', 'LocalCubicBezier', 'PiecewiseQuintic', 'SmoothingStrength', 'RetryStrengthScales', 'AllowFallbackToCoarsePath')) { + Assert-True (-not $configurationSource.Contains($legacyConfiguration)) "PathSmoothingConfiguration must not retain $legacyConfiguration." +} +Assert-True ($configurationSource.Contains('MinimumClearanceReserveMeters = 0d;')) 'The requested 0 m clearance-reserve default must remain.' + +$readme = Get-Content -Raw -Encoding UTF8 (Join-Path $PathSmoothingRoot 'README.md') +foreach ($heading in @( + '# PathSmoothing', + 'Module Overview', + 'File Structure', + 'Smoothing Data Flow', + 'Result Status and Publication Rules', + 'Coordinates and Units', + 'Minimal Call Example', + 'Detailed Usage Guide', + 'Fixture Reports and Visualization', + 'Common Errors', + 'First-Version Limits' +)) { + Assert-True ($readme.Contains($heading)) "README is missing required section: $heading" +} + +Write-Output 'PathSmoothing LocalG2-only layout checks passed.' diff --git a/ClumsyPilot/tests/verify_path_smoothing_png.ps1 b/ClumsyPilot/tests/verify_path_smoothing_png.ps1 new file mode 100644 index 0000000..1ef1bd8 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_png.ps1 @@ -0,0 +1,30 @@ +param( + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json') +) + +$ErrorActionPreference = 'Stop' + +$hostProject = Join-Path $PSScriptRoot 'PathSmoothingPngVerificationHost\PathSmoothingPngVerificationHost.csproj' +if (-not (Test-Path -LiteralPath $hostProject)) { + throw "Path smoothing PNG verification host was not found: $hostProject" +} + +$resolvedFixturePath = (Resolve-Path -LiteralPath $FixturePath).Path +& dotnet run --project $hostProject --no-restore -- $resolvedFixturePath +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'verify_path_smoothing_local_g2_diagnostic_visualization.ps1') +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$runnerPath = Join-Path $PSScriptRoot 'run_local_g2_diagnostic_visualization.ps1' +$runnerOutput = Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-png-smoke' +& powershell -ExecutionPolicy Bypass -File $runnerPath -OutputDirectory $runnerOutput +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +$primaryPng = Join-Path $runnerOutput '05-local-g2-diagnostic-candidate.png' +if (-not (Test-Path -LiteralPath $primaryPng)) { + throw "Local G2 diagnostic runner did not publish $primaryPng" +} + +Write-Output 'Path smoothing PNG checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_adapter.ps1 b/ClumsyPilot/tests/verify_planning_map_adapter.ps1 new file mode 100644 index 0000000..7e97700 --- /dev/null +++ b/ClumsyPilot/tests/verify_planning_map_adapter.ps1 @@ -0,0 +1,47 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } +$boundsType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm', $true) +$gridType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.EnvironmentGridMap', $true) +$circleType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.CircleObstacle', $true) +$rectangleType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.AxisAlignedRectangleObstacle', $true) +$rasterizer = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.MapObstacleRasterizer', $true) +$adapter = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapAdapter', $true) +$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]105, [single]0, [single]105)) +$grid = [Activator]::CreateInstance($gridType, @($bounds, [single]20)) +Assert-Equal $true ($grid.IsWorldInBounds([single]104.9, [single]104.9)) 'Last partial cell must be inside.' +Assert-Equal $false ($grid.IsWorldInBounds([single]105, [single]50)) 'XMax must be exclusive.' +Assert-Equal $true ($grid.IsOccupiedWorld([single]-0.1, [single]20)) 'Outside world must be occupied.' +$circle = [Activator]::CreateInstance($circleType, @([single]40, [single]40, [single]0)) +$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $circle)) | Out-Null +Assert-True ($grid.OccupiedCount -ge 4) 'Circle touching a grid intersection must conservatively occupy adjacent cells.' +$rectangle = [Activator]::CreateInstance($rectangleType, @([single]80, [single]100, [single]80, [single]100)) +$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $rectangle)) | Out-Null +Assert-True ($grid.IsOccupiedWorld([single]90, [single]90)) 'Rectangle must occupy its intersecting cells.' +$outside = [Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]10)) +$before = $grid.OccupiedCount +$rasterizer.GetMethod('Rasterize').Invoke($null, @($grid, $outside)) | Out-Null +Assert-Equal $before $grid.OccupiedCount 'Outside obstacle must not mark cells.' +$planning = $adapter.GetMethod('Create').Invoke($null, @($grid)) +Assert-True ($planning.IsOccupiedWorld(0.04, 0.04)) 'Planning map must use metre world coordinates.' +Assert-Equal 0.0 ($planning.GetConservativeObstacleDistanceMeters(0.04, 0.04)) 'Occupied cell clearance must be zero.' +Assert-Equal 0.0 ($planning.GetConservativeObstacleDistanceMeters(-1.0, 0.0)) 'Outside map clearance must be zero.' +$singleBounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000)) +$singleGrid = [Activator]::CreateInstance($gridType, @($singleBounds, [single]50)) +$singleCircle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0)) +$rasterizer.GetMethod('Rasterize').Invoke($null, @($singleGrid, $singleCircle)) | Out-Null +Assert-Equal 1 $singleGrid.OccupiedCount 'Zero radius circle at a cell center must occupy exactly one cell.' +$singlePlanning = $adapter.GetMethod('Create').Invoke($null, @($singleGrid)) +$singleDistance = $singlePlanning.GetConservativeObstacleDistanceMeters(1.275, 1.275) +$singleNearestCellCenterDistance = [Math]::Sqrt(0.25 * 0.25 + 0.25 * 0.25) +Assert-True ((-not [double]::IsNaN($singleDistance)) -and (-not [double]::IsInfinity($singleDistance))) 'Single occupied cell clearance must be finite.' +Assert-True ($singleDistance -le $singleNearestCellCenterDistance) 'Single occupied cell clearance must not exceed nearest occupied cell geometry distance.' +$emptyGrid = [Activator]::CreateInstance($gridType, @($bounds, [single]20)) +$emptyPlanning = $adapter.GetMethod('Create').Invoke($null, @($emptyGrid)) +Assert-True ([double]::IsPositiveInfinity($emptyPlanning.GetConservativeObstacleDistanceMeters(0.01, 0.01))) 'Empty map clearance must be positive infinity.' +$invalidResolutionRejected = $false +try { [Activator]::CreateInstance($gridType, @($bounds, [single]10)) | Out-Null } catch { $invalidResolutionRejected = $true } +Assert-True $invalidResolutionRejected 'Resolution below 20 mm must fail.' +Write-Output 'Planning map adapter checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_documentation.ps1 b/ClumsyPilot/tests/verify_planning_map_documentation.ps1 new file mode 100644 index 0000000..48156a5 --- /dev/null +++ b/ClumsyPilot/tests/verify_planning_map_documentation.ps1 @@ -0,0 +1,58 @@ +param([string]$MapRoot = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map')) + +$ErrorActionPreference = 'Stop' + +function Assert-Contains([string]$Content, [string]$Expected, [string]$Message) { + if (-not $Content.Contains($Expected)) { throw "$Message Missing=$Expected" } +} + +function Assert-DocumentationCount([string]$RelativePath, [int]$MinimumCount) { + $path = Join-Path $MapRoot $RelativePath + $content = Get-Content -LiteralPath $path -Raw + $count = [regex]::Matches($content, '/// ').Count + if ($count -lt $MinimumCount) { throw "Insufficient public API documentation in $RelativePath ExpectedAtLeast=$MinimumCount Actual=$count" } +} + +$readmePath = Join-Path $MapRoot 'README.md' +if (-not (Test-Path -LiteralPath $readmePath)) { throw "Map README is required: $readmePath" } +$readme = Get-Content -LiteralPath $readmePath -Raw +Assert-Contains $readme 'PlanningMapFactory' 'README must identify the public creation facade.' +Assert-Contains $readme 'IMapObstacleSource' 'README must explain unified obstacle sources.' +Assert-Contains $readme 'PlanningGridMap' 'README must explain the planning snapshot output.' +Assert-Contains $readme 'SourceVersion' 'README must explain cache invalidation versions.' +Assert-Contains $readme 'PlanningMapBuildStatus' 'README must explain explicit map build termination states.' +Assert-Contains $readme 'PNG' 'README must explain debug image output.' +Assert-Contains $readme 'TrapMap' 'README must describe the legacy-map boundary.' + +Assert-DocumentationCount 'PlanningMapFactory.cs' 2 +Assert-DocumentationCount 'PlanningMapRequest.cs' 5 +Assert-DocumentationCount 'PlanningMapBuildResult.cs' 9 +Assert-DocumentationCount 'Core\MapBuildRequest.cs' 4 +Assert-DocumentationCount 'Core\EnvironmentMapBuildResult.cs' 7 +Assert-DocumentationCount 'Core\MapBoundsMm.cs' 12 +Assert-DocumentationCount 'Core\EnvironmentGridMap.cs' 13 +Assert-DocumentationCount 'Core\EnvironmentMapBuilder.cs' 2 +Assert-DocumentationCount 'Obstacles\IMapObstacle.cs' 2 +Assert-DocumentationCount 'Obstacles\CircleObstacle.cs' 6 +Assert-DocumentationCount 'Obstacles\AxisAlignedRectangleObstacle.cs' 7 +Assert-DocumentationCount 'Obstacles\MapObstacleRasterizer.cs' 2 +Assert-DocumentationCount 'Sources\IMapObstacleSource.cs' 5 +Assert-DocumentationCount 'Sources\ManualObstacleSource.cs' 6 +Assert-DocumentationCount 'Sources\TwoLegProjectionInput.cs' 13 +Assert-DocumentationCount 'Sources\TwoLegObstacleSource.cs' 6 +Assert-DocumentationCount 'Sources\TwoLegObstacleProjector.cs' 2 +Assert-DocumentationCount 'Sources\ObstacleProjectionResult.cs' 8 +Assert-DocumentationCount 'Sources\ObstacleSourceStatus.cs' 5 +Assert-DocumentationCount 'Planning\PlanningGridMap.cs' 15 +Assert-DocumentationCount 'Planning\PlanningMapAdapter.cs' 2 +Assert-DocumentationCount 'Planning\ObstacleDistanceField.cs' 2 +Assert-DocumentationCount 'Planning\EuclideanDistanceTransform.cs' 2 +Assert-DocumentationCount 'Planning\PlanningMapCache.cs' 5 +Assert-DocumentationCount 'Test\MovementTest.MapTest.cs' 3 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportRequest.cs' 3 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExportResult.cs' 8 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageExporter.cs' 6 +Assert-DocumentationCount 'Test\Visualization\PlanningMapImageRenderer.cs' 2 +Assert-DocumentationCount 'Test\Visualization\ValidatedPngWriter.cs' 2 + +Write-Output 'Planning map documentation checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_factory.ps1 b/ClumsyPilot/tests/verify_planning_map_factory.ps1 new file mode 100644 index 0000000..2f3c763 --- /dev/null +++ b/ClumsyPilot/tests/verify_planning_map_factory.ps1 @@ -0,0 +1,88 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } +function Assert-False($Actual, [string]$Message) { if ($Actual) { throw $Message } } +function Assert-Null($Actual, [string]$Message) { if ($null -ne $Actual) { throw $Message } } +function Find-NonPublicInstanceMethod($Type, [string]$Name, [Type[]]$ParameterTypes) { + foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Instance,NonPublic')) { + if ($candidate.Name -ne $Name) { continue } + $parameters = $candidate.GetParameters() + if ($parameters.Length -ne $ParameterTypes.Length) { continue } + $matches = $true + for ($index = 0; $index -lt $parameters.Length; $index++) { + if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break } + } + if ($matches) { return $candidate } + } + return $null +} +$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' +$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) +$obstacleType = $assembly.GetType($ns + 'IMapObstacle', $true) +$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) +$circleType = $assembly.GetType($ns + 'CircleObstacle', $true) +$manualType = $assembly.GetType($ns + 'ManualObstacleSource', $true) +$twoLegInputType = $assembly.GetType($ns + 'TwoLegProjectionInput', $true) +$twoLegType = $assembly.GetType($ns + 'TwoLegObstacleSource', $true) +$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) +$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) +$mapResultType = $assembly.GetType($ns + 'PlanningMapBuildResult', $true) +$mapBuildStatusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) +$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $false) +$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $false) +Assert-True ($operationBudgetType -ne $null) 'PlanningOperationBudget must exist for shared planning cancellation and timeout handling.' +Assert-True ($operationStopReasonType -ne $null) 'PlanningOperationStopReason must exist for shared planning cancellation and timeout handling.' +Assert-True $operationStopReasonType.IsEnum 'PlanningOperationStopReason must be an enum.' +foreach ($expectedStopReason in @('None', 'Cancelled', 'TimedOut')) { + Assert-True ($operationStopReasonType.GetEnumNames() -contains $expectedStopReason) "PlanningOperationStopReason must contain $expectedStopReason." +} +Assert-True ($mapResultType.GetProperty('Status') -ne $null) 'PlanningMapBuildResult must expose an explicit build status.' +foreach ($expectedMapStatus in @('Success', 'Failed', 'Cancelled', 'TimedOut')) { + Assert-True ($mapBuildStatusType.GetEnumNames() -contains $expectedMapStatus) "PlanningMapBuildStatus must contain $expectedMapStatus." +} +$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]2000, [single]0, [single]2000)) +$obstacles = [Array]::CreateInstance($obstacleType, 1) +$obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]100)), 0) +$manual = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles)) +$twoLegInput = [Activator]::CreateInstance($twoLegInputType, @($true, [single]1000, [single]1000, [double]([Math]::PI / 2), [single]100, [single]0, [single]-100, [single]0, [single]40, 'test snapshot')) +$twoLeg = [Activator]::CreateInstance($twoLegType, @('two-leg', [long]1, $false, $twoLegInput)) +$sources = [Array]::CreateInstance($sourceType, 2); $sources.SetValue($twoLeg, 0); $sources.SetValue($manual, 1) +function New-Request($sourceArray, [bool]$allowEmpty) { $request = [Activator]::CreateInstance($requestType); $request.Bounds = $bounds; $request.ResolutionMm = [single]50; $request.ObstacleSources = $sourceArray; $request.AllowExplicitEmptyMap = $allowEmpty; return $request } +$factory = [Activator]::CreateInstance($factoryType) +$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null, + @([Threading.CancellationToken], [TimeSpan]), $null) +Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.' +$createWithBudget = Find-NonPublicInstanceMethod $factoryType 'Create' @($requestType, $operationBudgetType) +Assert-True ($createWithBudget -ne $null) 'PlanningMapFactory must expose an internal budget-aware Create overload.' +$cancelledSource = New-Object Threading.CancellationTokenSource +$cancelledSource.Cancel() +$cancelledBudget = $budgetConstructor.Invoke(@($cancelledSource.Token, [TimeSpan]::FromSeconds(1))) +$cancelled = $createWithBudget.Invoke($factory, @((New-Request $sources $false), $cancelledBudget)) +Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancelled map construction must retain the cancellation status.' +Assert-False $cancelled.Succeeded 'Cancelled map construction must not succeed.' +Assert-Null $cancelled.Map 'Cancelled map construction must not publish a map.' +Assert-Equal 'None' $cancelled.CacheHit.ToString() 'Cancelled map construction must not publish a cache hit.' +$first = $factory.Create((New-Request $sources $false)) +Assert-True $first.Succeeded 'Mixed source request must build.' +Assert-True $first.Map.PlanningReady 'Applied source geometry must make the map ready.' +Assert-True ($first.Map.IsOccupiedWorld(1.0, 1.1)) 'TwoLeg must project detection-time local coordinates into world metres.' +$second = $factory.Create((New-Request $sources $false)) +Assert-Equal 'Input' $second.CacheHit.ToString() 'Same snapshot must hit complete input cache.' +Assert-True ([object]::ReferenceEquals($first.Map, $second.Map)) 'Same snapshot must return the exact immutable map object.' +$manualVersionTwo = [Activator]::CreateInstance($manualType, @('manual', [long]2, $true, $obstacles)) +$sourcesVersionTwo = [Array]::CreateInstance($sourceType, 2); $sourcesVersionTwo.SetValue($manualVersionTwo, 0); $sourcesVersionTwo.SetValue($twoLeg, 1) +$third = $factory.Create((New-Request $sourcesVersionTwo $false)) +Assert-Equal 'Occupancy' $third.CacheHit.ToString() 'Version change with equal occupancy must reuse occupancy buffers.' +Assert-True ($third.Map.SnapshotId -gt $first.Map.SnapshotId) 'Occupancy reuse must still issue a fresh snapshot id.' +$emptySources = [Array]::CreateInstance($sourceType, 0) +$emptyBlocked = $factory.Create((New-Request $emptySources $false)) +Assert-True (-not $emptyBlocked.Map.PlanningReady) 'Implicit empty map must be blocked.' +$emptyAllowed = $factory.Create((New-Request $emptySources $true)) +Assert-True $emptyAllowed.Map.PlanningReady 'Explicit empty map must be accepted.' +$invalidManual = [Activator]::CreateInstance($manualType, @('invalid', [long]0, $true, $null)) +$invalidSources = [Array]::CreateInstance($sourceType, 1); $invalidSources.SetValue($invalidManual, 0) +$invalid = $factory.Create((New-Request $invalidSources $false)) +Assert-True (-not $invalid.Succeeded) 'Required invalid source must reject the full build.' +Write-Output 'Planning map factory checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_image.ps1 b/ClumsyPilot/tests/verify_planning_map_image.ps1 new file mode 100644 index 0000000..b674c57 --- /dev/null +++ b/ClumsyPilot/tests/verify_planning_map_image.ps1 @@ -0,0 +1,31 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } +$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.' +$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true) +$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true) +$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true) +$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true) +$imageRequestType = $assembly.GetType($ns + 'PlanningMapImageExportRequest', $true) +$exporterType = $assembly.GetType($ns + 'PlanningMapImageExporter', $true) +$mapRequest = [Activator]::CreateInstance($requestType) +$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]1000, [single]0, [single]1000)) +$mapRequest.ResolutionMm = [single]50 +$mapRequest.ObstacleSources = [Array]::CreateInstance($sourceType, 0) +$mapRequest.AllowExplicitEmptyMap = $true +$map = [Activator]::CreateInstance($factoryType).Create($mapRequest).Map +$imageRequest = [Activator]::CreateInstance($imageRequestType) +$imageRequest.Map = $map +$imageRequest.OutputRootDirectory = (Join-Path $PSScriptRoot '..\obj\planning_map_image_test') +$export = $exporterType.GetMethod('ExportIfEnabled').Invoke($null, @($true, $imageRequest)) +Assert-True $export.Saved ('Image export failed: ' + $export.Message) +Assert-True (Test-Path -LiteralPath $export.FilePath -PathType Leaf) 'Image output is missing.' +$png = [IO.File]::ReadAllBytes($export.FilePath) +Assert-True ($png.Length -gt 45) 'PNG is too small.' +Assert-True ($png[0] -eq 137 -and $png[1] -eq 80 -and $png[2] -eq 78 -and $png[3] -eq 71) 'PNG signature is invalid.' +Assert-True ([Text.Encoding]::ASCII.GetString($png, 12, 4) -eq 'IHDR') 'PNG must begin with IHDR.' +Assert-True ([Text.Encoding]::ASCII.GetString($png, $png.Length - 8, 4) -eq 'IEND') 'PNG must end with IEND.' +$source = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\Visualization\PlanningMapImageExporter.cs') -Raw -Encoding UTF8 +Assert-True ($source -notmatch 'GridMapData|TrapMapVehiclePose|TwoLegDetect') 'New exporter must consume only PlanningGridMap.' +Write-Output 'Planning map image checks passed.' diff --git a/ClumsyPilot/tests/verify_planning_map_test_config.ps1 b/ClumsyPilot/tests/verify_planning_map_test_config.ps1 new file mode 100644 index 0000000..ad52397 --- /dev/null +++ b/ClumsyPilot/tests/verify_planning_map_test_config.ps1 @@ -0,0 +1,23 @@ +param([string]$SourcePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\Map\Test\MovementTest.MapTest.cs')) + +$ErrorActionPreference = 'Stop' +$source = Get-Content -LiteralPath $SourcePath -Raw + +function Assert-Contains([string]$Expected, [string]$Message) { + if (-not $source.Contains($Expected)) { throw "$Message Missing=$Expected" } +} + +Assert-Contains 'EnableTerminalDebugLog' 'MapTest must expose a terminal logging switch.' +Assert-Contains 'SavePng' 'MapTest must expose a PNG export switch.' +$begin = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5F00, 0x59CB) +$sources = -join [char[]](0x969C, 0x788D, 0x7269, 0x6765) +$projection = -join [char[]](0x6295, 0x5F71, 0x7ED3, 0x679C) +$snapshot = -join [char[]](0x5730, 0x56FE, 0x5FEB, 0x7167) +$end = -join [char[]](0x89C4, 0x5212, 0x5730, 0x56FE, 0x521B, 0x5EFA, 0x5B8C, 0x6210) +Assert-Contains 'FormatSourceStatus' 'MapTest must translate projection status for terminal output.' +Assert-Contains 'FormatCacheHit' 'MapTest must translate cache hits for terminal output.' +Assert-Contains 'FormatImageResult' 'MapTest must translate image export results for terminal output.' +if ($source.Contains('PLANNING_MAP_CREATE_BEGIN')) { throw 'MapTest must not expose English terminal log markers.' } +Assert-Contains 'PlanningMapFactory' 'MapTest must continue using the public map facade.' + +Write-Output 'Planning map MovementTest configuration checks passed.' diff --git a/dailywork_report/.daily-summary-job/2026-08-03/checkpoints/120019-daily-summary-job.json b/dailywork_report/.daily-summary-job/2026-08-03/checkpoints/120019-daily-summary-job.json new file mode 100644 index 0000000..8550529 --- /dev/null +++ b/dailywork_report/.daily-summary-job/2026-08-03/checkpoints/120019-daily-summary-job.json @@ -0,0 +1,22 @@ +{ + "captured_at": "2026-08-03T12:00:19+08:00", + "date": "2026-08-03", + "module": "daily-summary-job", + "summary": "daily-summary-job 个人技能已完成设计、安装和自动化验证,当前进入真实项目试用阶段。", + "achievements": [ + "完成个人技能安装,并支持记录进展、生成日报和更新日报三类按需触发。", + "建立模块自动识别、目录自动初始化、日期命名和 Markdown/HTML 成对归档规则。", + "实现持久化精简检查点,支持从多 Agent 工作证据中逐步汇总而不依赖完整对话上下文。", + "实现自包含交互式 HTML,可筛选问题、切换现状/目标、查看原因链、解决步骤、验证门和路线图。", + "完成技能结构校验、31 项自动测试、JavaScript 语法检查和临时项目端到端验证。" + ], + "issues": [ + "Windows 默认 Python/PowerShell 输出编码可能导致中文路径和标签在控制台显示乱码;使用 python -X utf8 后恢复正常。", + "浏览器控制运行时当前无法启动,因此交互页面尚缺少真实浏览器截图和视觉回归确认。" + ], + "next_steps": [ + "让脚本在 Windows 默认以 UTF-8 输出,减少调用方对 python -X utf8 的依赖。", + "在可用浏览器中完成桌面端、移动端和键盘操作的视觉验收。", + "在后续真实开发日持续使用该技能,并根据实际日报反馈调整证据抽取与交互呈现。" + ] +} diff --git a/dailywork_report/.daily-summary-job/2026-08-03/summaries/daily-summary-job_rep/01-daily-summary-job-skill-progress-daily-summary.json b/dailywork_report/.daily-summary-job/2026-08-03/summaries/daily-summary-job_rep/01-daily-summary-job-skill-progress-daily-summary.json new file mode 100644 index 0000000..9e15855 --- /dev/null +++ b/dailywork_report/.daily-summary-job/2026-08-03/summaries/daily-summary-job_rep/01-daily-summary-job-skill-progress-daily-summary.json @@ -0,0 +1,238 @@ +{ + "date": "2026-08-03", + "title": "Daily Summary Job 技能今日进展总结", + "summary": "当前功能定位:daily-summary-job 是一个按需触发的个人开发日报技能,负责记录精简检查点、提取当天及多 Agent 工作证据、整理问题与改善、自动分类归档,并生成或更新成对的 Markdown 和自包含交互式 HTML。技能已经完成设计、安装与核心验证;结构校验、31 项自动测试、JavaScript 语法检查及独立临时项目端到端流程均已通过。当前仍需处理 Windows 控制台默认编码体验,并在可用的真实浏览器中补做视觉与交互验收。", + "modules": [ + "daily-summary-job" + ], + "achievements": [ + { + "title": "当前技能功能介绍(截至 2026-08-03)", + "detail": "daily-summary-job 当前提供八类能力:①按需识别自然语言或显式技能调用;②记录精简开发检查点;③汇总当天证据并生成日报;④按稳定问题 ID 合并新证据、更新原报告;⑤提取多 Agent 的完成项、问题、原因、验证和下一步,并保留冲突结论;⑥自动识别模块、初始化报告目录、按日期和序号命名;⑦用交互式 HTML 展现问题、原因链、改善步骤、验证门和路线图;⑧校验 Markdown/HTML 配对、证据一致性和离线自包含性。技能不在后台持续运行,也不要求固定口令。", + "evidence": [ + "C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md", + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py", + "docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md" + ] + }, + { + "title": "建立自动分类、初始化和日期归档机制", + "detail": "技能会优先使用显式模块,其次匹配已有 *_rep 目录;跨模块归入 cross-module_rep,无法判定或项目没有代码目录时归入 general_rep,并自动创建 dailywork_report 下所需目录。最终报告使用 YYYY-MM-DD 日期目录和顺序编号命名。", + "evidence": [ + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py", + "C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md" + ] + }, + { + "title": "实现面向多 Agent 工作的持久化检查点", + "detail": "通过每日检查点把完成项、问题和下一步压缩为结构化 JSON,最终日报优先汇总这些持久证据,从而避免为了生成日报长期保留所有 Agent 的完整对话。检查点预算限制为 5 个完成项、5 个问题和 3 个下一步。", + "evidence": [ + "C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md", + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py" + ] + }, + { + "title": "实现交互式问题与解决思路可视化", + "detail": "HTML 报告为无外部依赖的自包含页面,提供问题选择、证据等级筛选、现状/目标对比、原因链、解决步骤、改进前后切换、验证门、路线图、键盘导航、响应式布局和减少动画偏好支持。", + "evidence": [ + "C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html", + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py" + ] + }, + { + "title": "完成命令行流程与自动化验证", + "detail": "实现 inspect、checkpoint、render/update 和 validate 四类命令;此前的新鲜验证结果为技能结构有效、31 项测试全部通过、模板 JavaScript 语法通过,且独立临时项目中的记录、生成、更新复用同一文件对和最终校验均成功。", + "evidence": [ + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py", + "C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py", + "docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md" + ] + } + ], + "issues": [ + { + "id": "issue-windows-console-encoding", + "title": "Windows 默认控制台编码导致中文输出乱码", + "module": "daily-summary-job", + "evidence_level": "已验证", + "discovery": "本次执行 inspect 和读取中文字段时,普通 python/PowerShell 命令的控制台输出出现乱码;随后对源文件进行 UTF-8 解码和字符码点检查,确认文件没有损坏。", + "actual": "脚本生成的文件内容是正确 UTF-8,但默认命令行输出中的中文项目路径、模块名和证据标签可能不可读。", + "expected": "在 Windows 默认调用方式下,命令行 JSON 和诊断信息也应稳定显示为可读 UTF-8 中文。", + "cause": "Windows 当前 Python/PowerShell 控制台输出编码与脚本的 UTF-8 内容不一致;使用 python -X utf8 重跑相同命令后中文显示正常,说明问题位于进程输出编码层。", + "impact": "不会破坏已经写入的报告文件,但会降低命令行可读性,并可能影响依赖控制台文本进行人工核对或下游解析的流程。", + "improvements": [ + { + "action": "本次所有后续脚本命令改用 python -X utf8 执行。", + "result": "inspect 输出中的中文项目路径恢复正常,报告输入可以安全校验。", + "status": "已应用临时规避" + }, + { + "action": "对 SKILL.md、report-schema.md 和脚本证据等级常量执行 UTF-8 解码及字符检查。", + "result": "确认磁盘文件内容有效,乱码不是文件损坏造成。", + "status": "已验证" + } + ], + "validation": [ + { + "name": "UTF-8 文件解码", + "status": "通过", + "detail": "技能说明、报告模式和脚本均可按 UTF-8 正常读取,中文证据等级字符存在且码点正确。" + }, + { + "name": "UTF-8 模式重跑 inspect", + "status": "通过", + "detail": "python -X utf8 下相同 inspect 命令正确显示包含中文的项目路径。" + } + ], + "next_steps": [ + "在脚本入口显式配置 stdout/stderr 为 UTF-8,或在技能命令模板中统一启用 UTF-8 模式。", + "增加 Windows 默认控制台调用的回归测试,覆盖中文路径和中文 JSON 字段。" + ], + "evidence": [ + { + "label": "脚本入口", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py", + "result": "普通模式出现乱码,python -X utf8 模式显示正常。" + }, + { + "label": "UTF-8 内容检查", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md", + "result": "文件能够严格按 UTF-8 解码,证据等级文本正确。" + } + ] + }, + { + "id": "issue-browser-visual-qa-unavailable", + "title": "交互页面尚未完成真实浏览器视觉验收", + "module": "daily-summary-job", + "evidence_level": "待验证风险", + "discovery": "尝试使用应用内浏览器预览交互页面时,浏览器控制运行时因 Node 启动错误而不可用。", + "actual": "HTML 模板已经通过源码断言、JavaScript 语法检查和端到端生成校验,但没有在真实浏览器中完成截图、点击和响应式布局检查。", + "expected": "应在可工作的浏览器中打开生成报告,操作筛选器和切换器,并分别检查桌面端、移动端、键盘导航与减少动画模式。", + "cause": "当前应用内浏览器控制环境无法启动其 Node 运行时,属于本次技能代码之外的预览工具可用性问题。", + "impact": "交互结构和脚本语法已有自动化保障,但仍可能存在只有真实渲染后才能发现的间距、溢出、颜色对比或浏览器兼容问题。", + "improvements": [ + { + "action": "运行模板源码交互断言和 JavaScript 语法检查。", + "result": "核心控件、事件逻辑和脚本语法均通过检查。", + "status": "已完成" + }, + { + "action": "在独立临时项目中执行生成、更新和 validate 端到端流程。", + "result": "生成的 HTML 为自包含文件,更新复用原有文件对,最终校验有效。", + "status": "已完成" + } + ], + "validation": [ + { + "name": "自动化测试", + "status": "通过", + "detail": "31 项自动测试全部通过,覆盖模板关键结构和报告工作流。" + }, + { + "name": "JavaScript 语法", + "status": "通过", + "detail": "模板内联脚本通过 Node 语法检查。" + }, + { + "name": "真实浏览器视觉检查", + "status": "待验证", + "detail": "浏览器控制运行时无法启动,尚无真实渲染截图证据。" + } + ], + "next_steps": [ + "在可用浏览器中打开本次生成的可视化报告并逐项操作所有控件。", + "至少检查常见桌面宽度和窄屏移动端宽度,并记录截图或视觉问题。" + ], + "evidence": [ + { + "label": "交互模板", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html", + "result": "模板包含筛选、切换、原因链、解决步骤、验证门和路线图。" + }, + { + "label": "浏览器运行时错误", + "reference": "本次技能开发会话的浏览器预览尝试", + "result": "failed to start Node runtime,os error 3;无法形成视觉截图证据。" + } + ] + } + ], + "validations": [ + { + "name": "技能结构校验", + "status": "通过", + "detail": "quick_validate.py 返回 Skill is valid!。" + }, + { + "name": "自动化测试", + "status": "通过", + "detail": "test_prepare_report.py 共 31 项测试通过,0 失败。" + }, + { + "name": "模板脚本语法", + "status": "通过", + "detail": "抽取模板内联 JavaScript 后执行 node --check 通过。" + }, + { + "name": "独立临时项目端到端流程", + "status": "通过", + "detail": "记录、生成、更新和校验链路成功;更新复用了同一 Markdown/HTML 文件对,validation_valid 为 true。" + }, + { + "name": "真实浏览器视觉验收", + "status": "待验证", + "detail": "受应用内浏览器运行时故障影响,尚未完成真实渲染和交互截图检查。" + } + ], + "next_steps": [ + { + "priority": "P0", + "module": "daily-summary-job", + "action": "统一 Windows 命令行 UTF-8 输出行为,并补充中文路径回归测试。", + "done_when": "不附加 python -X utf8 时,inspect、checkpoint、render 和 validate 的中文 JSON 输出仍清晰可读。" + }, + { + "priority": "P1", + "module": "daily-summary-job", + "action": "在真实浏览器中完成交互与响应式视觉验收。", + "done_when": "桌面端和移动端均完成控件操作、键盘导航和布局检查,并留下截图或可复核记录。" + }, + { + "priority": "P2", + "module": "daily-summary-job", + "action": "在后续真实开发日持续记录检查点并更新日报,根据实际使用反馈调整抽取规则。", + "done_when": "至少完成一个包含多 Agent 证据的完整工作日闭环,并确认报告没有遗漏关键问题或产生事实混淆。" + } + ], + "sources": [ + { + "label": "技能说明", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md" + }, + { + "label": "报告数据模式", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md" + }, + { + "label": "报告生成脚本", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py" + }, + { + "label": "自动测试", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py" + }, + { + "label": "交互页面模板", + "reference": "C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html" + }, + { + "label": "设计文档", + "reference": "docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md" + }, + { + "label": "实施计划", + "reference": "docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md" + } + ] +} diff --git a/dailywork_report/.daily-summary-job/2026-08-04/summaries/emplanner_rep/01-emplanner-architecture-ls-optimization-daily-summary.json b/dailywork_report/.daily-summary-job/2026-08-04/summaries/emplanner_rep/01-emplanner-architecture-ls-optimization-daily-summary.json new file mode 100644 index 0000000..670230d --- /dev/null +++ b/dailywork_report/.daily-summary-job/2026-08-04/summaries/emplanner_rep/01-emplanner-architecture-ls-optimization-daily-summary.json @@ -0,0 +1,112 @@ +{ + "date": "2026-08-04", + "title": "EMPlanner 架构梳理与 LS 优化启动日报", + "summary": "已依据 EMPlanner 当前说明和代码目录明确轨迹规划的分层架构与模块边界;LS 横向优化为当前开展方向,尚未在本日报中宣称完成或通过专项回归。", + "modules": [ + "emplanner" + ], + "achievements": [ + { + "title": "确定 EMPlanner 轨迹规划分层架构", + "detail": "明确从请求快照、方向段/Frenet 投影和静态走廊,到 LS、ST、轨迹装配、世界空间复核及上层滚动执行的职责划分。", + "evidence": [ + "ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md" + ] + }, + { + "title": "明确规划器与执行侧边界", + "detail": "EMPlanner 仅产出经校验的时间轨迹;调度、轨迹交接、换向执行和控制命令由 TrajectoryExecution 承担。", + "evidence": [ + "ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md", + "ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md" + ] + }, + { + "title": "梳理 LS 优化的接口边界", + "detail": "LS 使用 l、dl、ddl、dddl 变量,在静态走廊与车辆几何约束下求解;求解器通过 IQpSolver/QuadraticProgram 抽象接入。", + "evidence": [ + "ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md", + "ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/" + ] + } + ], + "issues": [ + { + "id": "ls-optimization-in-progress", + "title": "LS 横向优化处于开展阶段", + "module": "emplanner", + "evidence_level": "对话发现", + "discovery": "用户说明当前正在开展 LS 优化工作。", + "actual": "当前已明确 LS 位于静态走廊之后、ST 之前,并承担横向路径优化与独立几何复核;本日报未取得本轮专项优化结果或回归输出。", + "expected": "在不破坏静态走廊、导数、Frenet 分母和车辆曲率硬约束的前提下,形成可独立复核的横向候选轨迹。", + "cause": "优化参数、目标权重、收敛策略及专项场景的本轮验证证据尚待补充。", + "impact": "LS 结果直接决定后续 ST 使用的实际 PathS 与轨迹可发布性,需先完成可行性和稳定性验证。", + "improvements": [], + "validation": [ + { + "name": "架构与接口静态核对", + "status": "通过", + "detail": "README 与 Lateral 目录表明 LS 的数据流、变量及求解器边界已定义;未执行 LS 专项运行测试。" + } + ], + "next_steps": [ + "确定 LS 优化目标、权重和约束调参方案。", + "针对典型前进、倒车及走廊边界场景执行专项回归。" + ], + "evidence": [ + { + "label": "LS 架构说明", + "reference": "ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md", + "result": "文档定义 LS SQP、独立横向几何复核及其与 ST 的数据边界。" + }, + { + "label": "当前工作说明", + "reference": "用户需求(2026-08-04)", + "result": "当前开展 LS 优化工作。" + } + ], + "conceptual_visual": true + } + ], + "validations": [ + { + "name": "EMPlanner 架构静态核对", + "status": "通过", + "detail": "核对 README、Lateral 目录和当日 EMPlanner 提交记录;本次未运行构建或自动化测试。" + } + ], + "next_steps": [ + { + "priority": "P0", + "module": "emplanner", + "action": "细化并实现 LS 优化策略", + "done_when": "横向优化目标、约束和候选筛选策略形成可审查实现。" + }, + { + "priority": "P1", + "module": "emplanner", + "action": "执行 LS 专项回归", + "done_when": "典型前进、倒车和窄走廊场景均有可复核的运行结果。" + }, + { + "priority": "P1", + "module": "emplanner", + "action": "联调 LS 到 ST 的 PathS 输入", + "done_when": "ST 仅消费 LS 已复核且严格递增的实际 PathS,并保留验证记录。" + } + ], + "sources": [ + { + "label": "EMPlanner 架构说明", + "reference": "ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md" + }, + { + "label": "当日 EMPlanner 提交记录", + "reference": "git log --since='2026-08-04 00:00' -- ClumsyPilot/ParkrobTrajplanner/EMPlanner" + }, + { + "label": "LS/ST 观测计划", + "reference": "docs/superpowers/plans/2026-08-04-em-observation-movementtest.md" + } + ] +} diff --git a/dailywork_report/Map_rep/.gitkeep b/dailywork_report/Map_rep/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dailywork_report/coarsepath_rep/.gitkeep b/dailywork_report/coarsepath_rep/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-report.md b/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-report.md new file mode 100644 index 0000000..3378474 --- /dev/null +++ b/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-report.md @@ -0,0 +1,92 @@ +# Daily Summary Job 技能今日进展总结 + +> 日期:2026-08-03 +> 模块:daily-summary-job +> 证据原则:已验证事实、静态分析、对话发现和待验证风险必须分开表达。 + +## 1. 今日结论摘要 + +当前功能定位:daily-summary-job 是一个按需触发的个人开发日报技能,负责记录精简检查点、提取当天及多 Agent 工作证据、整理问题与改善、自动分类归档,并生成或更新成对的 Markdown 和自包含交互式 HTML。技能已经完成设计、安装与核心验证;结构校验、31 项自动测试、JavaScript 语法检查及独立临时项目端到端流程均已通过。当前仍需处理 Windows 控制台默认编码体验,并在可用的真实浏览器中补做视觉与交互验收。 + +## 2. 今日完成的工作 + +- **当前技能功能介绍(截至 2026-08-03)**:daily-summary-job 当前提供八类能力:①按需识别自然语言或显式技能调用;②记录精简开发检查点;③汇总当天证据并生成日报;④按稳定问题 ID 合并新证据、更新原报告;⑤提取多 Agent 的完成项、问题、原因、验证和下一步,并保留冲突结论;⑥自动识别模块、初始化报告目录、按日期和序号命名;⑦用交互式 HTML 展现问题、原因链、改善步骤、验证门和路线图;⑧校验 Markdown/HTML 配对、证据一致性和离线自包含性。技能不在后台持续运行,也不要求固定口令。(证据:C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md、C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py、docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md) +- **建立自动分类、初始化和日期归档机制**:技能会优先使用显式模块,其次匹配已有 *_rep 目录;跨模块归入 cross-module_rep,无法判定或项目没有代码目录时归入 general_rep,并自动创建 dailywork_report 下所需目录。最终报告使用 YYYY-MM-DD 日期目录和顺序编号命名。(证据:C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py、C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md) +- **实现面向多 Agent 工作的持久化检查点**:通过每日检查点把完成项、问题和下一步压缩为结构化 JSON,最终日报优先汇总这些持久证据,从而避免为了生成日报长期保留所有 Agent 的完整对话。检查点预算限制为 5 个完成项、5 个问题和 3 个下一步。(证据:C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md、C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py) +- **实现交互式问题与解决思路可视化**:HTML 报告为无外部依赖的自包含页面,提供问题选择、证据等级筛选、现状/目标对比、原因链、解决步骤、改进前后切换、验证门、路线图、键盘导航、响应式布局和减少动画偏好支持。(证据:C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html、C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py) +- **完成命令行流程与自动化验证**:实现 inspect、checkpoint、render/update 和 validate 四类命令;此前的新鲜验证结果为技能结构有效、31 项测试全部通过、模板 JavaScript 语法通过,且独立临时项目中的记录、生成、更新复用同一文件对和最终校验均成功。(证据:C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py、C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py、docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md) + +## 3. 今日发现的问题 + +### 3.1 Windows 默认控制台编码导致中文输出乱码 `issue-windows-console-encoding` + +- **证据等级**:已验证 +- **实际现象**:脚本生成的文件内容是正确 UTF-8,但默认命令行输出中的中文项目路径、模块名和证据标签可能不可读。 +- **正确预期**:在 Windows 默认调用方式下,命令行 JSON 和诊断信息也应稳定显示为可读 UTF-8 中文。 +- **原因或当前假设**:Windows 当前 Python/PowerShell 控制台输出编码与脚本的 UTF-8 内容不一致;使用 python -X utf8 重跑相同命令后中文显示正常,说明问题位于进程输出编码层。 +- **影响**:不会破坏已经写入的报告文件,但会降低命令行可读性,并可能影响依赖控制台文本进行人工核对或下游解析的流程。 + +### 3.2 交互页面尚未完成真实浏览器视觉验收 `issue-browser-visual-qa-unavailable` + +- **证据等级**:待验证风险 +- **实际现象**:HTML 模板已经通过源码断言、JavaScript 语法检查和端到端生成校验,但没有在真实浏览器中完成截图、点击和响应式布局检查。 +- **正确预期**:应在可工作的浏览器中打开生成报告,操作筛选器和切换器,并分别检查桌面端、移动端、键盘导航与减少动画模式。 +- **原因或当前假设**:当前应用内浏览器控制环境无法启动其 Node 运行时,属于本次技能代码之外的预览工具可用性问题。 +- **影响**:交互结构和脚本语法已有自动化保障,但仍可能存在只有真实渲染后才能发现的间距、溢出、颜色对比或浏览器兼容问题。 + +## 4. 问题如何被发现及证据 + +### issue-windows-console-encoding · Windows 默认控制台编码导致中文输出乱码 + +本次执行 inspect 和读取中文字段时,普通 python/PowerShell 命令的控制台输出出现乱码;随后对源文件进行 UTF-8 解码和字符码点检查,确认文件没有损坏。 + +| 证据 | 引用 | 结果 | +| --- | --- | --- | +| 脚本入口 | C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py | 普通模式出现乱码,python -X utf8 模式显示正常。 | +| UTF-8 内容检查 | C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md | 文件能够严格按 UTF-8 解码,证据等级文本正确。 | + +### issue-browser-visual-qa-unavailable · 交互页面尚未完成真实浏览器视觉验收 + +尝试使用应用内浏览器预览交互页面时,浏览器控制运行时因 Node 启动错误而不可用。 + +| 证据 | 引用 | 结果 | +| --- | --- | --- | +| 交互模板 | C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html | 模板包含筛选、切换、原因链、解决步骤、验证门和路线图。 | +| 浏览器运行时错误 | 本次技能开发会话的浏览器预览尝试 | failed to start Node runtime,os error 3;无法形成视觉截图证据。 | + +## 5. 已采取的改善和验证结果 + +- **issue-windows-console-encoding / 已应用临时规避**:本次所有后续脚本命令改用 python -X utf8 执行。;结果:inspect 输出中的中文项目路径恢复正常,报告输入可以安全校验。 +- **issue-windows-console-encoding / 已验证**:对 SKILL.md、report-schema.md 和脚本证据等级常量执行 UTF-8 解码及字符检查。;结果:确认磁盘文件内容有效,乱码不是文件损坏造成。 +- **issue-windows-console-encoding / UTF-8 文件解码 / 通过**:技能说明、报告模式和脚本均可按 UTF-8 正常读取,中文证据等级字符存在且码点正确。 +- **issue-windows-console-encoding / UTF-8 模式重跑 inspect / 通过**:python -X utf8 下相同 inspect 命令正确显示包含中文的项目路径。 +- **issue-browser-visual-qa-unavailable / 已完成**:运行模板源码交互断言和 JavaScript 语法检查。;结果:核心控件、事件逻辑和脚本语法均通过检查。 +- **issue-browser-visual-qa-unavailable / 已完成**:在独立临时项目中执行生成、更新和 validate 端到端流程。;结果:生成的 HTML 为自包含文件,更新复用原有文件对,最终校验有效。 +- **issue-browser-visual-qa-unavailable / 自动化测试 / 通过**:31 项自动测试全部通过,覆盖模板关键结构和报告工作流。 +- **issue-browser-visual-qa-unavailable / JavaScript 语法 / 通过**:模板内联脚本通过 Node 语法检查。 +- **issue-browser-visual-qa-unavailable / 真实浏览器视觉检查 / 待验证**:浏览器控制运行时无法启动,尚无真实渲染截图证据。 +- **技能结构校验 / 通过**:quick_validate.py 返回 Skill is valid!。 +- **自动化测试 / 通过**:test_prepare_report.py 共 31 项测试通过,0 失败。 +- **模板脚本语法 / 通过**:抽取模板内联 JavaScript 后执行 node --check 通过。 +- **独立临时项目端到端流程 / 通过**:记录、生成、更新和校验链路成功;更新复用了同一 Markdown/HTML 文件对,validation_valid 为 true。 +- **真实浏览器视觉验收 / 待验证**:受应用内浏览器运行时故障影响,尚未完成真实渲染和交互截图检查。 + +## 6. 尚未解决的风险与下一步 + +- **issue-windows-console-encoding**:在脚本入口显式配置 stdout/stderr 为 UTF-8,或在技能命令模板中统一启用 UTF-8 模式。 +- **issue-windows-console-encoding**:增加 Windows 默认控制台调用的回归测试,覆盖中文路径和中文 JSON 字段。 +- **issue-browser-visual-qa-unavailable**:在可用浏览器中打开本次生成的可视化报告并逐项操作所有控件。 +- **issue-browser-visual-qa-unavailable**:至少检查常见桌面宽度和窄屏移动端宽度,并记录截图或视觉问题。 +- **P0 / daily-summary-job**:统一 Windows 命令行 UTF-8 输出行为,并补充中文路径回归测试。;完成标志:不附加 python -X utf8 时,inspect、checkpoint、render 和 validate 的中文 JSON 输出仍清晰可读。 +- **P1 / daily-summary-job**:在真实浏览器中完成交互与响应式视觉验收。;完成标志:桌面端和移动端均完成控件操作、键盘导航和布局检查,并留下截图或可复核记录。 +- **P2 / daily-summary-job**:在后续真实开发日持续记录检查点并更新日报,根据实际使用反馈调整抽取规则。;完成标志:至少完成一个包含多 Agent 证据的完整工作日闭环,并确认报告没有遗漏关键问题或产生事实混淆。 + +## 7. 证据索引 + +- **技能说明**:C:/Users/admin/.codex/skills/daily-summary-job/SKILL.md +- **报告数据模式**:C:/Users/admin/.codex/skills/daily-summary-job/references/report-schema.md +- **报告生成脚本**:C:/Users/admin/.codex/skills/daily-summary-job/scripts/prepare_report.py +- **自动测试**:C:/Users/admin/.codex/skills/daily-summary-job/scripts/test_prepare_report.py +- **交互页面模板**:C:/Users/admin/.codex/skills/daily-summary-job/assets/interactive-report-template.html +- **设计文档**:docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md +- **实施计划**:docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md diff --git a/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-visualization.html b/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-visualization.html new file mode 100644 index 0000000..24aa069 --- /dev/null +++ b/dailywork_report/daily-summary-job_rep/2026-08-03/01-daily-summary-job-skill-progress-daily-summary-visualization.html @@ -0,0 +1,397 @@ + + + + + + 开发工作日报|交互式问题与改善说明 + + + +
+
+
+

开发日报 · 问题与改善交互说明

+

今日工作总结

+

+
+ +
+ +
+
0完成事项
+
0发现问题
+
0验证记录
+
0下一步
+
+ +
+ + +
+
+ 对话发现 +

暂无问题

+

+
+
+ +
+
+

实际发生

+

+
+
+

正确预期

+

+
+
+ +
+

问题发现

+

实际现象

+

原因或假设

+

影响范围

+
+

概念示意:没有数值证据时不绘制虚构比例。

+ +
+
+

改善步骤

+
+
+
+

尚未记录改善

+

当前只保留问题和下一步,不把候选方案写成已完成。

+
+ + +
+
+
+
+
+
+ +
+

验证门

+
+
+ +
+

下一步路线图

+
+
+ +
本页与 Markdown 主报告来自同一结构化事实源。键盘左右方向键可切换问题。
+ + + + + + +
+ + + + + diff --git a/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-report.md b/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-report.md new file mode 100644 index 0000000..ca6e89f --- /dev/null +++ b/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-report.md @@ -0,0 +1,55 @@ +# EMPlanner 架构梳理与 LS 优化启动日报 + +> 日期:2026-08-04 +> 模块:emplanner +> 证据原则:已验证事实、静态分析、对话发现和待验证风险必须分开表达。 + +## 1. 今日结论摘要 + +已依据 EMPlanner 当前说明和代码目录明确轨迹规划的分层架构与模块边界;LS 横向优化为当前开展方向,尚未在本日报中宣称完成或通过专项回归。 + +## 2. 今日完成的工作 + +- **确定 EMPlanner 轨迹规划分层架构**:明确从请求快照、方向段/Frenet 投影和静态走廊,到 LS、ST、轨迹装配、世界空间复核及上层滚动执行的职责划分。(证据:ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md) +- **明确规划器与执行侧边界**:EMPlanner 仅产出经校验的时间轨迹;调度、轨迹交接、换向执行和控制命令由 TrajectoryExecution 承担。(证据:ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md、ClumsyPilot/ParkrobTrajplanner/TrajectoryExecution/README.md) +- **梳理 LS 优化的接口边界**:LS 使用 l、dl、ddl、dddl 变量,在静态走廊与车辆几何约束下求解;求解器通过 IQpSolver/QuadraticProgram 抽象接入。(证据:ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md、ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/) + +## 3. 今日发现的问题 + +### 3.1 LS 横向优化处于开展阶段 `ls-optimization-in-progress` + +- **证据等级**:对话发现 +- **实际现象**:当前已明确 LS 位于静态走廊之后、ST 之前,并承担横向路径优化与独立几何复核;本日报未取得本轮专项优化结果或回归输出。 +- **正确预期**:在不破坏静态走廊、导数、Frenet 分母和车辆曲率硬约束的前提下,形成可独立复核的横向候选轨迹。 +- **原因或当前假设**:优化参数、目标权重、收敛策略及专项场景的本轮验证证据尚待补充。 +- **影响**:LS 结果直接决定后续 ST 使用的实际 PathS 与轨迹可发布性,需先完成可行性和稳定性验证。 + +## 4. 问题如何被发现及证据 + +### ls-optimization-in-progress · LS 横向优化处于开展阶段 + +用户说明当前正在开展 LS 优化工作。 + +| 证据 | 引用 | 结果 | +| --- | --- | --- | +| LS 架构说明 | ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md | 文档定义 LS SQP、独立横向几何复核及其与 ST 的数据边界。 | +| 当前工作说明 | 用户需求(2026-08-04) | 当前开展 LS 优化工作。 | + +## 5. 已采取的改善和验证结果 + +- **ls-optimization-in-progress / 架构与接口静态核对 / 通过**:README 与 Lateral 目录表明 LS 的数据流、变量及求解器边界已定义;未执行 LS 专项运行测试。 +- **EMPlanner 架构静态核对 / 通过**:核对 README、Lateral 目录和当日 EMPlanner 提交记录;本次未运行构建或自动化测试。 + +## 6. 尚未解决的风险与下一步 + +- **ls-optimization-in-progress**:确定 LS 优化目标、权重和约束调参方案。 +- **ls-optimization-in-progress**:针对典型前进、倒车及走廊边界场景执行专项回归。 +- **P0 / emplanner**:细化并实现 LS 优化策略;完成标志:横向优化目标、约束和候选筛选策略形成可审查实现。 +- **P1 / emplanner**:执行 LS 专项回归;完成标志:典型前进、倒车和窄走廊场景均有可复核的运行结果。 +- **P1 / emplanner**:联调 LS 到 ST 的 PathS 输入;完成标志:ST 仅消费 LS 已复核且严格递增的实际 PathS,并保留验证记录。 + +## 7. 证据索引 + +- **EMPlanner 架构说明**:ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md +- **当日 EMPlanner 提交记录**:git log --since='2026-08-04 00:00' -- ClumsyPilot/ParkrobTrajplanner/EMPlanner +- **LS/ST 观测计划**:docs/superpowers/plans/2026-08-04-em-observation-movementtest.md diff --git a/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-visualization.html b/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-visualization.html new file mode 100644 index 0000000..d26ec91 --- /dev/null +++ b/dailywork_report/emplanner_rep/2026-08-04/01-emplanner-architecture-ls-optimization-daily-summary-visualization.html @@ -0,0 +1,419 @@ + + + + + + 开发工作日报|交互式问题与改善说明 + + + +
+
+
+

开发日报 · 问题与改善交互说明

+

今日工作总结

+

+
+ +
+ +
+
0完成事项
+
0发现问题
+
0验证记录
+
0下一步
+
+ +
+ + +
+
+ 对话发现 +

暂无问题

+

+
+
+ +
+
+

实际发生

+

+
+
+

正确预期

+

+
+
+ +
+

问题发现

+

实际现象

+

原因或假设

+

影响范围

+
+

概念示意:没有数值证据时不绘制虚构比例。

+ +
+
+

改善步骤

+
+
+
+

尚未记录改善

+

当前只保留问题和下一步,不把候选方案写成已完成。

+
+ + +
+
+
+
+
+
+ +
+

验证门

+
+
+ +
+

下一步路线图

+
+
+ +
本页与 Markdown 主报告来自同一结构化事实源。键盘左右方向键可切换问题。
+ + + + + + +
+ + + + + + diff --git a/dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md b/dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md new file mode 100644 index 0000000..e9556d2 --- /dev/null +++ b/dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md @@ -0,0 +1,183 @@ +# Local G2 五次 Hermite 路径平滑算法说明 + +> 文档定位:说明当前 Local G2 五次 Hermite 路径平滑任务的算法结构、输入、输出和实现边界。 +> 证据范围:截至 2026-07-31 的实现计划、代码结构和已运行专项检查。 + +## 1. 目标、位置与非目标 + +### 1.1 它要解决什么问题 + +Hybrid A* 粗路径由离散运动基元连接而成。它能提供可行的车辆空间路径,但相邻基元交界处可能出现车辆曲率的突变。曲率突变会使后续连续优化或控制模块得到不够平滑的初值。 + +Local G2 五次 Hermite 预平滑的目标,是只在这些局部曲率突变附近,用一小段五次 Hermite 曲线替换原有几何,使连接处的位置、切向趋势和曲率更连续,并且不牺牲车辆碰撞安全、净空和方向切换拓扑。 + +### 1.2 它在系统中的位置 + + 成功的 Hybrid A* 粗路径 + ↓ + Local G2 五次 Hermite 预平滑 + ↓ + 后续 SQP 或时间参数化模块的空间初始路径 + +这里的 Local 表示只修改必要的局部窗口;G2 表示曲线连接处不仅位置相连、切向连续,还要让曲率连续;五次 Hermite 表示每一小段曲线由两端的位置、一阶导数和二阶导数确定。 + +### 1.3 本算法不做什么 + +- 不重新搜索 Hybrid A*,不改变其代价、地图模型或运动基元生成策略。 +- 不跨越前进/倒车换向点做曲率计算或曲线拼接。 +- 不做 SQP 求解、速度规划、加速度规划、轮速分解或动态障碍物预测。 +- 不承诺当前代码已经把 Local G2 结果正式发布给下游。当前完成的是候选构造和评价层;专用发布流水线仍待接入。 + +## 2. 输入:进入算法前必须具备什么 + +Local G2 的业务入口设计基于一个路径平滑请求。它需要的输入如下。 + +| 输入 | 作用 | 关键要求 | +|---|---|---| +| 成功的 Hybrid A* 粗路径 | 给出原始空间轨迹与每个点的车辆曲率 | 路径点数值有限,弧长非递减 | +| 路径方向段 | 将前进段、倒车段隔离 | 段索引连续,段内方向一致,换向点不被跨越 | +| 地图 | 用于完整车体碰撞和净空复核 | 地图查询可用 | +| 车辆参数 | 提供车体外形、曲率上限和安全边界 | 曲率约束有效 | +| LocalG2QuinticOptions | 控制窗口、偏移、质量门和候选上限 | 所有参数通过有限值与范围校验 | +| 公共平滑配置 | 控制输出采样、碰撞步长、净空余量 | 输出采样和碰撞步长必须为正 | +| 取消令牌 | 允许耗时计算被安全取消 | 每个候选和区域循环均应检查 | + +### 2.1 坐标和单位 + +- 位置、路径长度、窗口长度、偏移和净空:米 m。 +- 航向:弧度 rad。 +- 车辆曲率 κ:1/m。 +- 曲率导数 dκ/ds:1/m²,表示沿路径走一米时曲率变化得有多快。 + +### 2.2 Local G2 的默认安全参数 + +| 参数 | 默认值 | 含义 | +|---|---:|---| +| 最小 / 偏好 / 最大窗口长度 | 0.20 / 0.50 / 0.80 m | 单个局部替换允许的长度范围 | +| 最大局部偏移 | 0.10 m | 候选曲线不得偏离原始窗口过远 | +| 曲率跳变阈值 | max(0.001,0.05 × 最大车辆曲率) | 过滤数值噪声,只处理足够明显的跳变 | +| 峰值 dκ/ds 最低改善 | 20% | 候选必须明显降低曲率变化尖峰 | +| 曲率变化代价容差 | 2% | 不允许用新的高频摆动换取表面平滑 | +| 每区域候选上限 | 12 | 保证计算量与结果可重复 | +| 输出采样 / 碰撞检查步长 | 0.025 / 0.025 m | 统一几何分析与连续车体复核的精度 | +| 最小净空余量 | 0.02 m | 候选与障碍物必须保留的安全距离 | + +## 3. 模块架构:每个模块负责什么 + +| 模块 | 职责 | 当前状态 | +|---|---|---| +| PathSmoothingPreprocessor | 校验粗路径,将其拆成不跨换向点的方向段,并保留真实起始车辆曲率 | 已实现 | +| PathGeometryAnalyzer | 统一重采样,恢复航向,计算 κ、dκ/ds、曲率变化代价等指标 | 已实现 | +| CurvatureTransitionDetector | 在同一方向段内检测相邻粗路径点的曲率跳变 | 已实现 | +| LocalG2WindowPlanner | 为跳变规划受限局部窗口,并合并可能相互影响的事件 | 已实现 | +| QuinticHermiteCurve2D | 由两端的位置、切向导数、二阶导数构造二维五次 Hermite 曲线 | 已实现 | +| LocalG2CandidateBuilder | 生成有限数量的候选曲线,处理前进/倒车曲率符号 | 已实现 | +| LocalG2PathSplicer | 将一个局部候选替换进完整方向段,同时保持换向拓扑 | 已实现 | +| LocalG2CandidateEvaluator | 执行偏移、曲率、碰撞、净空、改善度和确定性排序检查 | 已实现 | +| SmoothedPathValidator | 以完整车体模型复核候选路径和相邻点扫掠安全 | 已实现 | +| LocalG2PreSmoothingPipeline | 顺序处理全部区域、局部回退、全局回滚并发布最终结果 | 尚未实现 | +| PathSmoothingService 的 Local G2 分派 | 将 LocalG2Quintic 请求送入专用流水线 | 尚未接入 | + +## 4. 数据流:从粗路径到候选安全路径 + +### 步骤 1:预处理与方向隔离 + +粗路径先按前进和倒车拆成方向段。换向点两侧允许同一车辆位姿重复出现,但绝不被当作同一条连续曲线的一部分。这样可以避免把停车换向误判为需要平滑的几何折点。 + +### 步骤 2:检测曲率跳变 + +检测器只比较同一方向段内相邻粗路径点的车辆曲率。当差值达到阈值时,记录曲率事件:它所在的段、左右点、局部弧长、车辆航向,以及跳变前后的曲率。 + +### 步骤 3:规划局部窗口 + +窗口规划器围绕每个事件产生不同长度和左右分配的窗口方案。相邻事件若可能相互影响,会被归入同一个区域;每个区域最多输出 12 个确定性窗口/尺度组合,避免随机结果。 + +### 步骤 4:构造五次 Hermite 候选 + +对一个窗口,候选构造器在窗口两端以及必要的内部曲率事件处建立边界节点。每个节点包含位置、车辆航向、车辆曲率和导数尺度。对每对相邻节点构造一段五次 Hermite 曲线并按 0.025 m 左右的空间间距采样。 + +直观理解:原始路径像由短直线和圆弧拼出的折线化轨迹;五次 Hermite 曲线像在局部用一条可控的柔性曲线接住两边,使方向和转弯程度逐步过渡。 + +### 步骤 5:拼接候选 + +拼接器保留窗口之前和之后的原始点,用精确插值得到窗口端点,再插入候选点。随后重新计算该方向段的局部弧长,保持段首、段尾和换向标志不变。 + +### 步骤 6:统一几何分析和完整车体验证 + +候选路径与原始窗口都通过同一个几何分析器计算指标,再由完整车体碰撞检查器验证每个姿态和相邻姿态之间的扫掠。这样“看起来更圆滑”的曲线不会绕过实际车体安全检查。 + +### 步骤 7:质量门与选优 + +一个候选只有同时通过以下检查,才可被接受: + +1. 不超过车辆曲率上限,也不超出原始局部曲率范围。 +2. 到原始窗口折线的最大偏移不超过 0.10 m。 +3. 完整路径碰撞检查通过,最小净空不少于 0.02 m。 +4. 峰值 dκ/ds 至少降低 20%。 +5. 曲率变化代价不比原始窗口增加超过 2%。 + +若多个候选合格,按最大偏移、峰值 dκ/ds、曲率变化代价、路径长度变化和候选索引依次排序,确保结果可重复。 + +## 5. 输出:路径、段、指标、区域报告与状态 + +### 5.1 候选层已经能够产生的结果 + +候选评价层可以得到一条经过完整车体复核的候选安全路径,以及对应的方向段和局部指标。每个输出路径点包含: + +- 位置、航向、展开航向和全局弧长; +- 前进或倒车方向; +- 几何曲率、车辆曲率、车辆曲率导数 dκ/ds; +- 重新计算的车体净空; +- 是否为换向点,以及该点来自原始锚点、插值点或 Local G2 过渡点。 + +每个方向段则记录其段号、方向、路径点的起止索引,以及换向拓扑标志。 + +### 5.2 计划中的正式发布结果 + +公共契约已为 Local G2 预留以下最终状态: + +| 状态 | 计划含义 | +|---|---| +| NotNeeded | 未检测到需要处理的曲率跳变,发布已验证的原始路径 | +| Complete | 所有区域都得到有效改善 | +| PartialImprovement | 只有部分区域得到有效改善,其余区域保留原样 | +| Unchanged | 检测到事件,但没有候选能安全通过质量门 | + +正式结果还应包含每个区域的报告:窗口范围、候选数、被选候选、原始/结果峰值 dκ/ds、曲率变化代价、最大偏移、最小净空和保留原路径的原因。 + +### 5.3 当前输出边界 + +上述最终状态和区域报告的契约已经存在,但负责顺序处理多个区域、必要时全局回滚、并由服务发布结果的 LocalG2PreSmoothingPipeline 尚未实现。因此,当前代码的准确描述是“可生成和评价局部 G2 候选”,而不是“已经对外发布完整 Local G2 平滑路径”。 + +## 6. 安全与质量门 + +Local G2 的核心原则不是尽可能把曲线画圆,而是在严格约束下只接受确实更好的局部替换。 + +- **方向安全**:不跨换向点,前进/倒车的曲率符号转换只做一次。 +- **几何安全**:候选端点必须与原始窗口精确对应,避免拼接留下缝隙或重复点。 +- **车辆安全**:逐点与逐段扫掠检查完整车体,而不是只检查轨迹中心线。 +- **净空安全**:以本次检查的净空为准,不信任候选输入自带的净空值。 +- **平滑质量**:以 dκ/ds 的峰值和曲率变化代价衡量改善,避免引入新的摆动。 +- **确定性**:固定窗口、固定导数尺度顺序和固定排序规则,便于复现与诊断。 + +## 7. 当前实现进度与边界 + +实施计划共 9 个任务。任务 1 至任务 7 已完成公共契约、真实起始曲率保留、统一 dκ/ds 分析、事件检测、窗口规划、五次 Hermite 几何、候选拼接和质量评价。 + +已重新执行并通过的专项检查包括: + +- 路径平滑契约检查; +- 粗路径集成与搜索检查; +- 路径平滑几何、验证器、服务和比较检查; +- Local G2 跳变检测、五次曲线和候选质量检查。 + +任务 8 的专用流水线、服务分派和 Local G2 端到端夹具检查尚未实现;任务 9 的完整回归与主路线文档也尚未完成。另有一个共享原始基线回归会在第二份报告中单独说明:RectangleDetour 场景在进入 G2 前的基线复验失败。 + +## 8. 证据索引 + +- Local G2 实施计划:docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md +- Local G2 设计说明:docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md +- Local G2 实现目录:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ +- 统一几何分析:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/PathGeometryAnalyzer.cs +- 完整车体验证:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Validation/SmoothedPathValidator.cs +- 专项检查:ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1、verify_path_smoothing_local_g2_curve.ps1、verify_path_smoothing_local_g2_candidates.ps1 diff --git a/dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md b/dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md new file mode 100644 index 0000000..9a5f0ad --- /dev/null +++ b/dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md @@ -0,0 +1,185 @@ +# Local G2 路径平滑问题分析与后续措施 + +> 文档定位:记录当前 Local G2 五次 Hermite 任务在进入正式服务接入前发现的问题、证据边界和下一步行动。 +> 核心原则:专项测试通过,说明某个局部能力有效;它不自动等同于真实场景下的端到端功能已经完成。 + +## 1. 阅读本报告前:证据等级说明 + +本报告使用三种证据等级,避免把不同确定性的结论混在一起。 + +| 证据等级 | 含义 | 本报告中的问题 | +|---|---|---| +| 已复现故障 | 已通过实际构建和测试命令稳定观察到失败 | 问题一 | +| 静态分析确认的逻辑缺口 | 从当前代码的输入、约束和控制流可以直接推导出不一致 | 问题二 | +| 待验证集成风险 | 当前代码结构在未来接入流程中可能导致错误,但尚无完整流水线可实际复现 | 问题三 | + +已经通过的专项检查包括:Local G2 契约、曲率导数、跳变检测、五次曲线、候选构造、候选质量门,以及共享的粗路径与平滑基础检查。本报告不否定这些检查的价值,而是说明它们尚未覆盖的组合场景。 + +## 2. 问题一:RectangleDetour 原始基线复验失败 + +### 2.1 现象 + +这是一个 **已复现故障**。 + +在端到端检查中,RectangleDetour 场景的 Hybrid A* 粗路径规划成功;但平滑模块建立“原始粗路径基线”时失败,结果从预期的 Success 变成 InvalidInput。实际测试输出为: + + Raw baseline RectangleDetour must remain a feasible, verified copy of the coarse path. + Expected=Success Actual=InvalidInput + +失败发生在 Local G2 候选生成之前。因此当前不能说“G2 平滑失败了”;更准确的说法是:这条真实粗路径还没有通过进入任何平滑算法前的统一基线复验。 + +### 2.2 生动例子 + +把路径平滑想成给一条已经规划出的山路做局部铺装。 + +RectangleDetour 这条山路的导航系统已经证明车辆能走到终点,相当于 Hybrid A* 已经找到了路。但在铺装前,工程人员用更严格的尺子重新测量道路的弯曲程度和车身扫过的范围。测量门禁认为这段路有不满足要求的地方,于是道路在进入铺装工序前被拦下。 + +此时不应强行开始铺装,也不应为了让它通过而降低安全门禁;先要查清是哪一个测量点、哪一种曲率或几何计算触发了拒绝。 + +### 2.3 为什么会出现这个现象 + +任务 3 将原始粗路径也接入与候选曲线相同的几何分析器和完整车体验证器,这是为了实现公平比较。当前链路为: + + 粗路径 → 预处理与方向分段 → 统一重采样和曲率分析 + → 完整车体/扫掠复验 → 原始基线 + +当前配置把输出采样间距设为 0.025 m;基线会在该间距下重新计算曲率和曲率导数,再进行车辆曲率、姿态和扫掠检查。已取得的诊断只说明“平滑路径包含非法数值或超限车辆曲率”,还没有定位到具体样本、具体方向段或根本原因。 + +因此可以确认的是“基线复验失败”;尚不能确认是原始粗路径本身超限、重采样的端点处理问题,还是某个几何分析/验证边界条件的问题。 + +### 2.4 影响 + +- RectangleDetour 无法进入 Local G2,也无法进入其他依赖同一基线的平滑比较流程。 +- 任务 8 的专用流水线应把已验证原始基线视为前提,因此此问题必须优先解决。 +- 如果简单放松基线检查,可能把真正不可执行的路径传给后续 SQP 或控制模块,风险不可接受。 + +### 2.5 下一步措施 + +1. 为 RectangleDetour 建立最小诊断复现:记录每个方向段的原始点、重采样点、最大绝对车辆曲率、车辆曲率上限、最小净空和验证失败位置。 +2. 对同一粗路径比较原始点序列与 0.025 m 重采样后的几何结果,定位第一次超限或非有限值出现的位置。 +3. 根据证据只修复源头:若粗路径本身确实违反车辆约束,修复粗路径输出或场景;若重采样/端点分析失真,修复分析器并保留完整安全门。 +4. 将该场景加入专门回归,防止以后再次出现“规划成功但基线不可用”的断链。 + +### 2.6 验收条件 + +- RectangleDetour 的粗路径仍需保持规划成功。 +- 原始基线必须回到 Success,且仍经过相同的完整车体和扫掠复验。 +- 诊断测试能够指出首次异常的方向段和样本,而不是只返回笼统的 InvalidInput。 +- 不通过放宽曲率、碰撞或净空门限来获得绿色结果。 + +## 3. 问题二:窗口合并范围与候选长度上限不一致 + +### 3.1 现象 + +这是一个 **静态分析确认的逻辑缺口**。 + +窗口规划器用“事件位置左右各扩展最大窗口长度”的包络判断两个事件是否合并。默认最大窗口长度为 0.8 m,因此合并判断可使用约 1.6 m 宽的影响范围。 + +但实际生成候选窗口时,候选的总长度又被限制为不超过 0.8 m。于是两个相距较远、但仍落入合并包络的事件可能被合并为一个区域;合并后却没有一条长度不超过 0.8 m 的窗口能够同时覆盖它们,最终该区域没有合法候选。 + +### 3.2 生动例子 + +想象道路上有两个需要修补的小坑: + +- 第一个坑在 0.4 m 处; +- 第二个坑在 1.4 m 处; +- 两坑相距 1.0 m。 + +系统先用一个很大的“可能影响范围”判断两坑会互相影响,于是把它们装进同一个维修任务。随后维修车被规定一次只能铺 0.8 m 长的补丁。要同时盖住相距 1.0 m 的两个坑,补丁至少要 1.0 m 长,因此没有任何合法补丁。 + +本来可以分别处理两个坑,结果因为过早合并,两者都没有被处理。 + +### 3.3 为什么会出现这个现象 + +当前窗口规划存在两套长度语义: + +- 合并阶段的最大影响范围使用 event ± 0.8 m; +- 候选阶段的最大总窗口长度使用 0.8 m。 + +如果“0.8 m”定义为总窗口长度,这两套语义不一致。合并阶段把每一侧都当作可以扩展 0.8 m,而候选阶段只能让左右两侧合计为 0.8 m。 + +现有检测测试覆盖了两个非常接近、确实应该合并的事件;尚未覆盖“会被合并,但无法被一个最大长度窗口同时覆盖”的中间距离场景。 + +### 3.4 影响 + +- 某些本可分别平滑的局部曲率跳变会被整体保留。 +- 任务 8 若直接消费空候选区域,可能把它报告为候选生成失败,掩盖真正的窗口规划逻辑问题。 +- 不同场景下的改善率可能不稳定:不是因为曲线质量差,而是因为区域边界定义不一致。 + +### 3.5 下一步措施 + +1. 先明确 0.8 m 的唯一语义:它是候选总长度,还是允许每一侧各 0.8 m。现有配置和候选生成逻辑均把它当作总长度。 +2. 增加窗口规划回归:两个相距 1.0 m 的事件在默认参数下应保持为两个可独立处理的区域,或在明确允许更长联合窗口时生成实际可用的联合候选。 +3. 让合并包络依据真实可生成的最大窗口范围计算,而不是使用大于候选总长度的宽泛包络。 +4. 对区域中候选数为零的情况提供稳定诊断,区分“没有可行曲线”和“窗口规划把可处理事件合并过度”。 + +### 3.6 验收条件 + +- 新增的 1.0 m 间距回归测试稳定通过。 +- 每个检测到的区域要么有至少一个符合长度约束的候选窗口,要么有明确且正确的不可用原因。 +- 既有重叠事件合并测试、近起点非对称窗口测试仍通过。 + +## 4. 问题三:连续处理多个区域时的弧长定位风险 + +### 4.1 现象 + +这是一个 **待验证集成风险**,不是已经在完整 Local G2 流水线中复现的故障。 + +局部拼接后,拼接器会根据新的几何形状重新计算整个方向段的局部弧长。候选评价器则会使用候选保存的起止弧长在当前路径中提取窗口。未来任务 8 需要顺序处理多个区域;若前一个区域改变了长度,后一个区域若仍使用原始弧长坐标,就可能定位到错误的位置。 + +### 4.2 生动例子 + +一条道路上有区域 A 和区域 B: + +- 原始道路中,区域 A 位于 1.0–1.5 m; +- 区域 B 位于 3.0–3.5 m; +- 修补区域 A 后,道路局部变长了 0.06 m。 + +如果系统仍在“新道路”的 3.0–3.5 m 处寻找区域 B,它实际上会比原始区域 B 早 0.06 m 找到位置。轻则候选端点对不上而被拒绝,重则替换到了错误的局部区间。 + +上面的 0.06 m 只是帮助理解的示意数值,并非已经测得的实际误差。 + +### 4.3 为什么会出现这个现象 + +候选构造使用原始方向段和原始窗口的起止弧长。拼接器替换一处后会重算当前方向段内所有点的弧长。候选评价器随后从当前路径按候选的旧起止弧长提取窗口,并尝试再次拼接。 + +任务 8 的设计本身要求顺序保留已接受的早期替换,因此它必须明确回答一个问题:后续区域应使用原始参考弧长,还是当前路径弧长?当前候选层尚没有负责这类坐标映射的专用机制。 + +### 4.4 影响 + +- 同一方向段含多个彼此分离的曲率事件时,后续区域可能不能被正确处理。 +- 结果可能依赖区域处理顺序,破坏确定性。 +- 即使每个单独候选都能通过专项检查,组合处理时仍可能出现端点错配、错误替换或不必要回退。 + +### 4.5 下一步措施 + +1. 在任务 8 之前定义稳定坐标策略:可以保留原始参考弧长并建立“原始弧长到当前路径位置”的映射,也可以在每次成功拼接后重新定位剩余区域和重新生成候选。 +2. 新增双区域顺序替换回归:同一方向段内两个不重叠区域,第一处替换必须产生可观测长度变化,第二处仍必须命中原始语义上的正确窗口。 +3. 在测试中重复运行两次,比较状态、路径点坐标、区域报告和被选候选索引,验证处理顺序和结果确定性。 +4. 只有上述回归通过后,才实现任务 8 的多区域发布与全局回滚。 + +### 4.6 验收条件 + +- 区域 A 被接受且路径长度变化后,区域 B 的窗口端点仍与其原始参考位置对应。 +- 两区域替换后,完整路径通过相同的碰撞、净空、曲率和拓扑验证。 +- 同一输入连续运行两次,输出状态、点数、坐标和区域报告一致。 + +## 5. 进入任务 8 前的行动顺序与验收条件 + +| 优先级 | 行动 | 原因 | 完成标志 | +|---|---|---|---| +| P0 | 定位并修复 RectangleDetour 原始基线复验 | 没有可信原始基线,任何 G2 发布都失去安全前提 | 场景基线 Success,诊断可定位问题来源 | +| P1 | 补充窗口合并边界回归并统一 0.8 m 语义 | 防止可独立平滑的事件被错误合并后放弃 | 1.0 m 间距场景有正确区域/候选结果 | +| P1 | 补充双区域顺序替换回归并确定弧长坐标策略 | 防止多区域处理发生错位 | 第二个区域命中正确语义窗口且结果确定 | +| P2 | 实现 LocalG2PreSmoothingPipeline 和服务分派 | 将候选层能力变为可发布的完整空间初始路径 | Local G2 端到端夹具、服务和完整回归全部通过 | + +在 P0、P1 未完成前,不应把 Local G2 标为“可供下游正式消费”的平滑主路线。 + +## 6. 证据索引 + +- 端到端失败检查:ClumsyPilot/tests/verify_path_smoothing_integration.ps1 +- 原始基线构建:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Processing/RawPathBaselineBuilder.cs +- 窗口规划:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs +- 局部拼接:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PathSplicer.cs +- 候选评价:ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs +- Local G2 实施计划:docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md diff --git a/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-report.md b/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-report.md new file mode 100644 index 0000000..da5c476 --- /dev/null +++ b/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-report.md @@ -0,0 +1,128 @@ +# Local G2 最小曲率越界可行性验证报告 + +> 文档定位:记录 Local G2 最小曲率越界(curvature excursion)受控探测的结论、改动边界和后续动作,供路径平滑模块的日常协作查阅。 +> +> 证据边界:本报告只摘要正式证据报告中的固定 SingleTurn 场景结果。它不是生产发布说明,也不把一次性 probe 中的测量改动描述为共享生产代码改动。 + +对应可视化附录:[03-local-g2-curvature-excursion-feasibility-visualization.html](03-local-g2-curvature-excursion-feasibility-visualization.html)。 + +## 1. 结论摘要 + +本轮最小曲率越界可行性探测的最终状态是 **QUALITY_RED**。它给出的授权结论很明确:**不修改生产 evaluator(候选评估器)**。 + +这不是因为运行超时,也不是因为证据无效。候选搜索、性能和重复运行均完成了预定验证;问题在于,没有候选同时满足严格接纳条件,因此没有可发布的替换路径。 + +| 项目 | 本轮结果 | 正确解读 | +| --- | --- | --- | +| 严格合格候选 | 0 | 原始严格范围条件下没有可接纳候选。 | +| 物理约束合格候选 | 3 | 仅说明它们满足该层物理约束,不能等同于最终接受。 | +| 曲率越界合格候选 | 0 | 没有可报告的最小可行越界值。 | +| 管线输出 | Unchanged | 原路径没有被替换。 | +| 性能超限 | False | 本轮未因性能失败。 | +| 证据有效性 | valid | 本轮结论可复核,但结论是不能修改 evaluator。 | + +## 2. 本轮做了什么 + +本轮不是在共享生产源码中直接“放宽曲率”,而是在一次性、可丢弃的 probe core 中回答一个更小的问题: + +> 在不改变严格生产接纳语义的前提下,是否存在一个能消除局部曲率突变、并以最小必要越界满足全部质量门的候选? + +验证分为两条互相隔离的路径: + +1. **Strict 路径**:保持共享 evaluator 的原始严格语义,判定候选能否被正式接纳。 +2. **Measurement 路径**:仅为了记录被严格 raw 曲率范围提前阻断后的后续质量门状态,允许诊断继续走到后面的检查点。它不是生产实现,也不改变 Strict 路径的结论。 + +这样可以把“候选的几何和车辆物理约束是否可行”与“候选是否允许进入正式输出”分开观察,避免为了得到绿色结果而直接放宽生产门槛。 + +## 3. 本轮修改范围 + +| 范围 | 改动内容 | 边界与结果 | +| --- | --- | --- | +| 一次性 probe core | 增加受控搜索、候选账本、严格审计和证据生成能力。 | 只用于本轮验证,不回写共享生产源码。 | +| Measurement root | 仅改动 LocalG2CandidateEvaluator.cs 中 raw 范围提前返回后的测量延续与 trace 捕获。 | 用于定位后续质量门;不是生产 evaluator 改动。 | +| 严格 root | 保持 evaluator 与共享基线一致。 | 严格结论仍是 0 个可接纳候选。 | +| 共享分支 | 发布正式证据报告。 | 没有提交 probe 源码,也没有修改生产 evaluator。 | +| 本日报目录 | 新增本 Markdown 摘要和对应离线 HTML 可视化。 | 仅整理结论与证据入口,不改变路径平滑行为。 | + +相关身份如下: + +| 身份 | 提交 | +| --- | --- | +| 共享基线 | da9157b17a7e4206ed4cdebd1985ee2577fae480 | +| 冻结 core / Strict | 3006aa0fd8dac8e4665ff3b89b7cda139e88e940 | +| Measurement | c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825 | +| 共享分支正式证据报告 | 67f86581a06f754ae166c6145478f454afefbf17 | + +## 4. 候选搜索结果 + +固定 SingleTurn 场景中的搜索预算和结果如下: + +| 指标 | 结果 | +| --- | ---: | +| 规划窗口数 | 8 | +| 已尝试组合数 | 24 | +| solver 调用数 | 30 | +| 已生成候选数 | 6 | +| 失败组合数 | 18 | +| 最大 interval 数 | 2 | +| 搜索停止原因 | None | +| 严格合格 / 物理约束合格 / 越界合格 | 0 / 3 / 0 | + +因此,本轮不是“没有生成任何曲线”。相反,系统生成了 6 个候选,其中 3 个通过了车辆物理约束层;但没有一个通过严格接纳层,也没有一个成为可报告的最小曲率越界方案。 + +## 5. 为什么最终没有路径变得更平滑 + +结论不能简化为“候选曲率一定比原路径更大”。更准确的过程是: + +1. 候选生成器确实构造了局部替换曲线。 +2. Strict 路径先检查 raw 曲率范围。候选没有满足这一严格范围条件,因此严格合格数为 0。 +3. Measurement 路径只为观察决策链而继续记录后续门。3 个物理约束合格候选仍在净空门被阻断,未进入最终质量评价和接受流程。 +4. 没有候选被选中,预平滑管线保持 Unchanged,原路径不替换。 + +这里有两个不同层次的概念: + +| 层次 | 回答的问题 | 本轮状态 | +| --- | --- | --- | +| 车辆物理约束 | 车辆最大曲率、接缝、偏移等基础几何约束是否可行? | 有 3 个候选通过。 | +| 严格接纳与质量门 | 候选是否能在现有生产语义下安全、完整地替换原路径? | 没有候选通过。 | + +所以“物理约束合格”不是“平滑方案已经有效”。当前实际效果是:**候选层被执行并留下可复核证据,但没有改变输出路径。** + +## 6. 性能与确定性 + +性能验证先进行 5 次预热,再进行 30 次测量运行。30 次测量只得到 1 个确定性签名,说明在该固定场景与固定环境下结果没有随机漂移。 + +| 指标 | 中位数 | 最坏值 | 限制 | 结果 | +| --- | ---: | ---: | ---: | --- | +| Solver 时间 | 4.93545 ms | 7.7368 ms | 10 / 25 ms | 通过 | +| Region 时间 | 10.09185 ms | 23.2227 ms | 25 / 50 ms | 通过 | + +性能通过只说明这次受控探测没有超过时间预算;它不能覆盖严格接纳失败,也不能把 QUALITY_RED 转为可发布状态。 + +## 7. 这份结果说明什么,不说明什么 + +| 可以说明 | 不能说明 | +| --- | --- | +| 当前固定 SingleTurn 场景中,严格语义下不存在可接纳的最小曲率越界候选。 | 所有地图、所有转弯或全部 Local G2 场景都不可行。 | +| 结果不是因超时或随机性造成。 | 只要放宽 evaluator 就应当发布。 | +| Measurement 路径有助于定位 raw 范围之后的净空阻断。 | Measurement 改动已经进入生产 evaluator。 | +| 当前授权边界是“不修改 evaluator”。 | 前两份报告中的基线、窗口语义和多区域弧长风险已经解决。 | + +特别需要保留的边界是:前两份日报中记录的 RectangleDetour 基线复验、窗口合并语义和多区域弧长定位问题,不会因为本次单场景 probe 结束而自动关闭。 + +## 8. 后续动作 + +1. 保持 QUALITY_RED 的授权边界:不把 Measurement 改动复制到生产 evaluator。 +2. 不以“变绿”为目的放宽 raw 曲率范围、净空或其他质量门。 +3. 继续按已有优先级处理基线复验、窗口长度语义和多区域弧长定位风险。 +4. 若要评估新的几何策略或新的场景范围,应另起独立计划,重新定义候选、质量门和验收证据,而不是从本次结果直接推导发布结论。 + +## 9. 证据索引 + +| 证据 | 用途 | +| --- | --- | +| docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md | 本日报所有数值和最终授权边界的正式来源。 | +| 3006aa0fd8dac8e4665ff3b89b7cda139e88e940 | 冻结 core 与 Strict 验证身份。 | +| c92c9c5b8e1995f61d4685f9a6ee23a6f8d5f825 | Measurement 专用、可丢弃的 evaluator trace 身份。 | +| 67f86581a06f754ae166c6145478f454afefbf17 | 共享分支上发布正式证据报告的提交。 | +| 03-local-g2-curvature-excursion-feasibility-visualization.html | 本报告的离线交互式可视化附录。 | diff --git a/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-visualization.html b/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-visualization.html new file mode 100644 index 0000000..d807109 --- /dev/null +++ b/dailywork_report/pathsmoothing_rep/03-local-g2-curvature-excursion-feasibility-visualization.html @@ -0,0 +1,616 @@ + + + + + + Local G2 最小曲率越界可行性验证|结果可视化 + + + +
+
+

离线结果可视化 · Local G2

+

最小曲率越界探测:没有新的可采纳路径

+

固定 SingleTurn 场景中,候选搜索、性能和确定性验证均已完成;但严格接纳条件下没有候选通过,预平滑管线保持原样。

+
+ QUALITY_RED + 性能通过 + 生产 evaluator 不变 +
+
+ + + +
+
+
+

候选已经生成,但没有进入最终输出

+

6 个候选中有 3 个达到车辆物理约束层;严格 raw 曲率范围合格数仍为 0,因此不能把任何候选当作可发布的平滑路径。

+
+

结果:Unchanged

+
+ +
+
+
    +
  1. 6已生成候选30 次 solver 调用
  2. +
  3. 3物理约束合格基础几何层
  4. +
  5. 0严格范围合格raw 曲率范围
  6. +
  7. 0越界合格无最小值可报告
  8. +
  9. 0替换输出管线 Unchanged
  10. +
+
+ +
+
+
+ 步骤 1 +

候选生成

+

局部替换曲线被构造并进入审核。

+ 6 个候选 +
+
+ 步骤 2 +

Strict raw 曲率范围

+

严格路径首先判定候选是否保持原有接纳语义。

+ 0 个严格合格 +
+
+ 步骤 3 +

Measurement trace

+

只为取证而继续记录后续质量门,不改变 Strict 结论。

+ 一次性测量路径 +
+
+ 步骤 4 +

净空与最终输出

+

3 个物理约束合格候选在净空门阻断,未被接受。

+ 原路径保持 +
+
+

Measurement 的作用是把阻断位置记录完整,不是放宽生产质量门。它不能授权修改 evaluator,也不能让候选进入正式输出。

+
+ +
+
+
+
+ Solver 中位 / 限制 +
+ 4.94 ms + + 限制 10 ms +
+
+
+ Solver 最坏 / 限制 +
+ 7.74 ms + + 限制 25 ms +
+
+
+ Region 中位 / 限制 +
+ 10.09 ms + + 限制 25 ms +
+
+
+ Region 最坏 / 限制 +
+ 23.22 ms + + 限制 50 ms +
+
+
+ +
+
+
+
+ +
+

如何理解“平滑方案效果”

+

本次结果不是“完全没有曲线”,也不能直接写成“所有候选曲率都比原路径更大”。真正的结论是,候选在不同质量层的状态不同,且没有一个最终满足现有严格接纳条件。

+
+
+

候选层确实执行

+

系统生成了 6 个局部候选,并对它们完成了有界搜索和账本审计。

+
+
+

严格接纳没有通过

+

raw 曲率范围严格合格数为 0,因此不能把任何候选写成可发布的路径平滑结果。

+
+
+

输出没有变化

+

Measurement 继续记录后,候选仍被净空门阻断;管线最终保持 Unchanged。

+
+
+
+ +
+

本轮修改边界

+ + + + + + + + + + +
范围改动结论
一次性 probe core候选搜索、账本、严格审计与证据生成。只用于验证,不回写生产源码。
Measurement rootLocalG2CandidateEvaluator.cs 的测量延续与 trace 捕获。只用于取证,不是生产 evaluator 变更。
共享分支发布正式证据报告。没有提交 probe 源码,也没有修改 evaluator。
本日报目录新增本页及对应 Markdown 摘要。仅整理事实与证据入口,不改变路径平滑行为。
+
+ +
+

后续边界

+
    +
  1. 保持 QUALITY_RED 对应的授权结论:不把 Measurement 改动复制到生产 evaluator。
  2. +
  3. 不为了得到绿色结果而放宽 raw 曲率范围、净空或其他质量门。
  4. +
  5. 继续按已有优先级处理基线复验、窗口长度语义与多区域弧长定位风险。
  6. +
  7. 新的几何策略或场景范围需要独立计划和新的验收证据,不能从本次 SingleTurn 结果直接推导发布结论。
  8. +
+
+ +
+ 对应主报告:03-local-g2-curvature-excursion-feasibility-report.md。数据来源:docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md。单文件离线 HTML,无外部资源依赖。 +
+
+ + + + diff --git a/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md b/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md new file mode 100644 index 0000000..a2781ee --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md @@ -0,0 +1,266 @@ +# TrapMap Image Export and Console Logging Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add optional 300 DPI full-map PNG export with hard pixel/file limits and an independent switch for TrapMap terminal diagnostics. + +**Architecture:** A new `TrapMapImageExporter` renders the completed in-memory grid without depending on CycleGUI/Painter state. `TrapMapTest` owns the two manual switches and calls the exporter only after successful map construction. A shared `TrapMapLog` always writes `DLog` and conditionally mirrors the same message to `Console.WriteLine`. + +**Tech Stack:** C# 10, .NET Standard 2.0, internal pure-C# RGBA rasterizer, exact `StbImageWriteSharp` 1.16.7 managed PNG encoder, BCL-only PowerShell PNG parser, existing TrapMap tests. + +## Global Constraints + +- Do not inspect or modify `TrajPlanner`. +- Do not commit or stage any file. +- Keep `UI.GetPainter("TrapMapTest")` as the world-coordinate Painter; do not reintroduce `false`. +- Keep `UI.GetPainter("MultiWheelTwoLegDetect.Filter", false)` as the car-coordinate ROI Painter. +- Defaults: `_saveFullMapImage = true`, `_enableTerminalDebugLog = true`. +- PNG: 300 DPI, 4 pixels per cell, maximum edge 4000 pixels, maximum final size `50 * 1024 * 1024` bytes. +- Output: `\TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png`. +- Export failure never changes `TrapMapBuilder.Succeeded` or clears `GridMap`. +- Do not capture the Clumsy viewport or add point-cloud/motion behavior. + +--- + +### Task 1: Export contract, dependency, and pre-allocation limits + +**Files:** +- Modify: `ClumsyPilot/ClumsyPilot.csproj` +- Create: `ClumsyPilot/TrapMapImageExporter.cs` +- Create: `ClumsyPilot/tests/verify_trapmap_image.ps1` + +**Interfaces:** +- Consumes: `GridMapData`, `TrapMapVehiclePose`, workstation `Vector2`, tire metadata, output root. +- Produces: `TrapMapImageExportRequest`, `TrapMapImageExportResult`, and `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)`. + +- [ ] **Step 1: Add a failing image-export reflection test** + +Create `verify_trapmap_image.ps1`. Load `ClumsyPilot/bin/Debug/netstandard2.0/ClumsyPilot.dll`; require types `MultiWheelC.TrapMapImageExporter`, `TrapMapImageExportRequest`, and `TrapMapImageExportResult`, and assert the assembly/output no longer contains a platform drawing dependency. Create a temporary directory under `$env:TEMP`, invoke the disabled path with an output directory that does not exist, and assert `Saved=false`, `Skipped=true`, and that no directory was created. Invoke an oversized request using a `1000×1` grid at 50mm so the four-pixels-per-cell canvas plus padding exceeds 4000, and assert rejection before any PNG/temp file exists. + +Use these assertion helpers and cleanup guard: + +```powershell +function Assert-Equal($expected, $actual, [string]$message) { + if ($expected -ne $actual) { throw "$message Expected=$expected Actual=$actual" } +} +$testRoot = Join-Path $env:TEMP ("trapmap-image-test-" + [guid]::NewGuid().ToString('N')) +try { + # reflection setup and assertions +} finally { + if (Test-Path -LiteralPath $testRoot) { + Remove-Item -LiteralPath $testRoot -Recurse -Force + } +} +``` + +- [ ] **Step 2: Run RED verification** + +```powershell +dotnet restore ClumsyPilot\ClumsyPilot.csproj +dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 +``` + +Expected: compilation succeeds and the image script fails because `TrapMapImageExporter` is absent. + +- [ ] **Step 3: Add the managed PNG dependency** + +Add the exact managed encoder package inside the package `ItemGroup` in `ClumsyPilot.csproj`. Keep the target framework unchanged, expose the package path, and use one explicit build target that copies only its single managed `netstandard2.0` runtime asset. No drawing-runtime or native asset is required: + +```xml + + + + + + +``` + +Do not change the target framework. + +- [ ] **Step 4: Implement request/result types and dimension validation** + +Create `TrapMapImageExporter.cs` in namespace `MultiWheelC`. Use exact constants: + +```csharp +public const int PixelsPerCell = 4; +public const int MaximumImageEdgePixels = 4000; +public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; +public const float OutputDpi = 300f; +public const int OuterPaddingPixels = 24; +public const int HeaderHeightPixels = 140; +``` + +Request properties must include `GridMap`, `VehiclePose`, `WorkstationWorld`, `TireLayerStatus`, `TireLayerMessage`, `InputSource`, and `OutputRootDirectory`. Result properties must include `Saved`, `Skipped`, `FilePath`, `Message`, `FileSizeBytes`, `PixelWidth`, and `PixelHeight`. + +`ExportIfEnabled(false, request)` returns a skipped result before validating the request or touching the filesystem. Enabled export validates non-null map/pose, finite workstation, then computes with `long`: + +```csharp +long pixelWidth = 2L * OuterPaddingPixels + (long)grid.Cols * PixelsPerCell; +long pixelHeight = HeaderHeightPixels + 2L * OuterPaddingPixels + + (long)grid.Rows * PixelsPerCell; +``` + +Reject non-positive or over-4000 dimensions before allocating the RGBA surface. Add `public static bool IsFileSizeAllowed(long byteCount)` returning `byteCount >= 0 && byteCount <= MaximumFileSizeBytes`; the actual save path must call this same function. + +- [ ] **Step 5: Run GREEN contract checks** + +Run the Task 1 commands. Expected: disabled/oversized assertions pass, no output directory exists for disabled export, and no RGBA buffer is allocated for oversized export. + +### Task 2: Complete 300 DPI PNG rendering and atomic 50MB save + +**Files:** +- Modify: `ClumsyPilot/TrapMapImageExporter.cs` +- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` + +**Interfaces:** +- Consumes: validated Task 1 request. +- Produces: a complete PNG or a failure result with no final/temporary file. + +- [ ] **Step 1: Add failing PNG behavior assertions** + +Extend the test with a `2×2` 50mm grid, mark one known occupied cell, set a finite vehicle/workstation, and export twice. Assert: + +```powershell +Assert-Equal $true $result.Saved 'Small map must save.' +Assert-Equal $false $result.Skipped 'Enabled successful export is not skipped.' +if (-not (Test-Path -LiteralPath $result.FilePath)) { throw 'PNG file missing.' } +if ((Get-Item -LiteralPath $result.FilePath).Length -gt 50MB) { throw 'PNG exceeds 50MB.' } +if ([IO.Path]::GetExtension($result.FilePath) -ne '.png') { throw 'Output is not PNG.' } +if ((Split-Path $result.FilePath -Leaf) -notmatch '^TrapMap_\d{8}_\d{6}_\d{3}(_\d+)?\.png$') { + throw 'Timestamp filename is invalid.' +} +$image = Read-PngRgba $result.FilePath +Assert-Equal $result.PixelWidth $image.Width 'PNG width mismatch.' +Assert-Equal $result.PixelHeight $image.Height 'PNG height mismatch.' +Assert-Equal 11811 $image.PixelsPerMetreX 'PNG horizontal pHYs mismatch.' +Assert-Equal 11811 $image.PixelsPerMetreY 'PNG vertical pHYs mismatch.' +``` + +Assert the two file paths differ. Assert `IsFileSizeAllowed(50MB)` is true and `IsFileSizeAllowed(50MB + 1)` is false. Assert no `*.tmp` remains. + +- [ ] **Step 2: Run RED behavior test** + +Run the image test. Expected: it fails because enabled rendering/save is not implemented. + +- [ ] **Step 3: Render the full grid** + +After validation, create an internal RGBA8 surface and draw cells, grid, overlays, and 5×7 bitmap text with clipped integer primitives. Encode the buffer with `StbImageWriteSharp`, then insert a CRC-protected `pHYs` chunk containing `11811,11811,1` immediately after `IHDR`. + +Exact layout and colors: + +```text +Canvas background: White +Map top-left: (OuterPaddingPixels, HeaderHeightPixels + OuterPaddingPixels) +Unmarked cell interior: White +Grid lines: LightGray, 1px +Occupied cell interior: Red +Map border: Black, 2px +Vehicle outline/center/heading: Blue +Workstation circle/cross/text: LimeGreen +Header text: Black +``` + +For cell `(col,row)`, invert Y: + +```csharp +int imageCol = col; +int imageRow = grid.Rows - 1 - row; +int x = mapLeft + imageCol * PixelsPerCell; +int y = mapTop + imageRow * PixelsPerCell; +``` + +Fill occupied interiors before drawing all vertical/horizontal grid lines. Convert vehicle rectangle corners and workstation through a shared world-to-pixel helper using `(worldX - XMin) / ResolutionMm` for X and `((YMin + Rows * ResolutionMm) - worldY) / ResolutionMm` for Y. The Y expression uses the discrete raster's actual upper edge, so overlays stay aligned when the requested world bounds are not an exact multiple of the resolution. Draw title strings for bounds, resolution, rows/cols, occupancy, obstacle count, tire status/message, and input source. + +- [ ] **Step 4: Implement collision-safe atomic save and cleanup** + +Create `\TrapMapExports` only after all request/dimension validation. Select the millisecond timestamp name; if it exists, append `_1`, `_2`, etc. Encode into the exclusively reserved `finalPath + ".tmp"` stream, read `FileInfo.Length`, call `IsFileSizeAllowed`, delete the temp on rejection, then `File.Move(tempPath, finalPath)`. + +Wrap rendering/saving in `try/catch/finally`; `finally` deletes only the current temp path if present. Never delete an existing final PNG. Return failure messages instead of throwing into the test runner. + +- [ ] **Step 5: Run image and existing behavior tests** + +```powershell +dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 +``` + +Expected: PNG assertions pass and existing behavior scripts retain their passing messages. + +### Task 3: Terminal switch and successful-map export integration + +**Files:** +- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs` +- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1` +- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` + +**Interfaces:** +- Consumes: Task 2 exporter. +- Produces: `_saveFullMapImage`, `_enableTerminalDebugLog`, shared dual-channel logging, and post-success export. + +- [ ] **Step 1: Add failing source/integration contracts** + +Require exact defaults, `EnableTerminalDebugLog` builder wiring, `TrapMapLog.Write`, and `TrapMapImageExporter.ExportIfEnabled`. Assert source still contains both world-Painter calls without `false`, retains the filter Painter with `false`, and export invocation occurs only after `_builder.Succeeded` is checked. + +Add source assertions: + +```powershell +if ($source -notmatch '_saveFullMapImage\s*=\s*true') { $failures.Add('Image switch default missing.') } +if ($source -notmatch '_enableTerminalDebugLog\s*=\s*true') { $failures.Add('Terminal switch default missing.') } +if ($source -notmatch 'TrapMapLog\.Write\(') { $failures.Add('Shared TrapMap logger missing.') } +if ($source -notmatch 'Console\.WriteLine\(') { $failures.Add('Terminal mirror missing.') } +if ($source -match 'GetPainter\("TrapMapTest",\s*false\)') { $failures.Add('TrapMap Painter regressed to local coordinates.') } +if ($source -notmatch 'GetPainter\("MultiWheelTwoLegDetect\.Filter",\s*false\)') { $failures.Add('Filter Painter lost local coordinates.') } +``` + +- [ ] **Step 2: Run RED contract test** + +Run `verify_trapmap_inputs.ps1`. Expected: new switch/logger/export assertions fail. + +- [ ] **Step 3: Implement the shared logger and switches** + +Add a `TrapMapLog` static class with: + +```csharp +public static void Write(string message, bool enableTerminal) +{ + DLog.Log(message, "TrapMapTest"); + if (enableTerminal) + Console.WriteLine($"[TrapMapTest] {message}"); +} +``` + +Add `public bool EnableTerminalDebugLog = true` to the builder. Replace each TrapMap-owned two-argument `DLog.Log` call whose category is exactly `"TrapMapTest"` with `TrapMapLog.Write(message, EnableTerminalDebugLog)` in the builder and with the const switch in `TrapMapTest`. Do not replace unrelated log categories or `Hedingben.ToastText`. + +Add the exact two constants to the test manual-edit section and pass terminal configuration into the builder. + +- [ ] **Step 4: Invoke image export after successful map construction** + +After the `_builder.Succeeded` failure return and after retrieving `grid`, construct the request from `_builder.GridMap`, `VehiclePose`, `WorkstationWorld`, tire status/message/input source, and `Environment.CurrentDirectory`. Call: + +```csharp +var export = TrapMapImageExporter.ExportIfEnabled(_saveFullMapImage, request); +TrapMapLog.Write(export.Message, _enableTerminalDebugLog); +if (export.Saved) + Hedingben.ToastText($"栅格图片已保存: {export.FilePath}", "TrapMapTest"); +``` + +Do not alter builder success when export fails/skips. + +- [ ] **Step 5: Run full fresh verification** + +```powershell +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1 +dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 +git diff --check +$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" } +``` + +Expected: source, compile, grid, lifecycle, and image tests pass; no whitespace errors; no staged files. Confirm an exporter-generated test PNG reports 300 DPI and never exceeds 50MB before test cleanup. diff --git a/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md b/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md new file mode 100644 index 0000000..4df0c26 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-trap-map-pure-png.md @@ -0,0 +1,125 @@ +# TrapMap Managed PNG Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove TrapMap's platform-specific drawing dependency and produce the same bounded 300 DPI PNG with a managed encoder that runs inside Clumsy. + +**Architecture:** Keep `TrapMapImageExporter.ExportIfEnabled` and its file-reservation/publication behavior. Replace only the renderer with an internal RGBA raster surface, primitive drawing functions, a compact embedded bitmap font, and `StbImageWriteSharp` for PNG encoding; insert the 300 DPI `pHYs` chunk after encoding. + +**Tech Stack:** C# 10, .NET Standard 2.0, `StbImageWriteSharp` 1.16.7, PowerShell contract/PNG parsing tests. + +## Global Constraints + +- Do not inspect or modify `TrajPlanner`. +- Do not commit or stage any file. +- Do not change grid construction, Painter behavior, movement behavior, switches, output location, naming, 300 DPI, 4 pixels per cell, 4000-pixel edge limit, or 50 MiB limit. +- The only new image package is `StbImageWriteSharp` version 1.16.7; do not add native assets or another graphics package. +- `TrapMapImageExporter` must have no runtime reference to `System.Drawing.Common` or `System.Drawing`. +- Preserve collision-safe temporary-file reservation, encoded-size validation, atomic publication, and contained export failures. + +--- + +### Task 1: Replace System.Drawing rendering with a managed Stb PNG encoder + +**Files:** +- Modify: `ClumsyPilot/ClumsyPilot.csproj` +- Modify: `ClumsyPilot/TrapMapImageExporter.cs` +- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1` +- Modify: `docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md` +- Modify: `docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md` + +**Interfaces:** +- Preserve: `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)` and all public request/result properties and constants. +- Add only private implementation units: `RgbaSurface`, integer drawing helpers, bitmap-font helpers, and `PngWriter`. +- `RenderToTemporaryPng` continues to consume the existing request/dimensions and write to the already exclusively reserved stream. + +- [ ] **Step 1: Add dependency-removal and PNG-structure assertions** + +Update `verify_trapmap_image.ps1` before production code. Require that: + +```powershell +if ($project.PackageReference.Include -contains 'System.Drawing.Common') { + throw 'TrapMap must not depend on System.Drawing.Common.' +} +if ($project.Target.Name -contains 'DeployFrameworkDrawingRuntime') { + throw 'Legacy drawing-runtime deployment target remains.' +} +if (-not ($project.PackageReference | Where-Object { + $_.Include -eq 'StbImageWriteSharp' -and $_.Version -eq '1.16.7' +})) { throw 'Exact managed PNG package is missing.' } +if ($exporterSource -match 'System\.Drawing|\bBitmap\b|\bGraphics\b|ImageFormat') { + throw 'Exporter still uses the external drawing API.' +} +``` + +Parse the generated PNG without loading a drawing assembly. Verify signature, one `IHDR`, one `pHYs`, one or more `IDAT`, and `IEND`; verify every chunk CRC. Assert `IHDR` width/height and RGBA8 fields and `pHYs` values `11811,11811,1`. Decode representative pixels with a test-only PNG decoder or the Stb package and reuse the existing color/Y-inversion assertions. + +Add a clean-output assertion after build: + +```powershell +$drawingDll = Join-Path (Split-Path -Parent $AssemblyPath) 'System.Drawing.Common.dll' +if (Test-Path -LiteralPath $drawingDll) { + throw 'System.Drawing.Common.dll must not be deployed for TrapMap.' +} +``` + +- [ ] **Step 2: Run RED verification** + +```powershell +dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 +``` + +Expected: the image test fails because the package, deployment target, `using System.Drawing`, and renderer still exist. + +- [ ] **Step 3: Remove the drawing package and deployment target** + +Delete the `System.Drawing.Common` `PackageReference` and the entire `DeployFrameworkDrawingRuntime` target. Add ``. Do not change the target framework or other references. Ensure a clean build cannot retain the old DLL: the verification command must remove only `ClumsyPilot/bin/Debug/netstandard2.0/System.Drawing.Common.dll` before rebuilding, after resolving and validating that exact path is under the project output directory. + +- [ ] **Step 4: Implement the RGBA raster surface** + +Replace drawing types with a private surface backed by `byte[]` in RGBA order. Required primitives and semantics: + +```csharp +SetPixel(int x, int y, byte r, byte g, byte b, byte a = 255); +FillRectangle(int x, int y, int width, int height, Color32 color); +DrawLine(int x0, int y0, int x1, int y1, Color32 color, int thickness); +DrawRectangle(int x, int y, int width, int height, Color32 color, int thickness); +DrawCircle(int centerX, int centerY, int radius, Color32 color, int thickness); +FillCircle(int centerX, int centerY, int radius, Color32 color); +FillPolygon(PointD[] points, Color32 color); +``` + +Clip every primitive to the surface. Use pre-clipped Bresenham lines, scale-normalized scanline polygon filling, and a canvas-clipped bounded circle scan whose work is proportional to visible rows/columns rather than radius. Preserve exact white, red, LightGray `(211,211,211)`, black, blue, and LimeGreen `(0,255,0)` colors. Convert vehicle/workstation world positions using the existing discrete-grid-aligned transform. + +- [ ] **Step 5: Implement deterministic bitmap text** + +Embed a private 5×7 ASCII glyph table for code points 32–126. Draw scaled glyphs using integer pixels; unsupported characters render as `?`. Use a 2× scale for header text and 1× scale for the workstation label. Keep all five header baselines inside `HeaderHeightPixels=140` with fixed non-overlapping line boxes. Continue building the same five metadata lines, including occupancy rate; sanitize only the exported header text, not logs. + +- [ ] **Step 6: Encode PNG and add 300 DPI metadata** + +Use `StbImageWriteSharp.ImageWriter.WritePng` to encode the RGBA buffer. Then insert: + +```text +pHYs: X=11811, Y=11811, unit=1 +``` + +Encode into a temporary `MemoryStream`, validate the PNG signature and first `IHDR` chunk, then copy the signature+IHDR, append the 13-byte `pHYs` chunk, and copy the remaining encoded chunks. Use big-endian integers and standard CRC-32 over `pHYs`+data. Do not close the caller-owned reserved stream before the existing file-size/atomic-move flow finishes. + +- [ ] **Step 7: Update documentation and run GREEN verification** + +Remove all claims that TrapMap deploys or requires `System.Drawing.Common`; document the pure C# encoder and BCL-only runtime. + +Run from a clean output state: + +```powershell +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1 +dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1 +powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1 +git diff --check +$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" } +``` + +Expected: build succeeds without a drawing DLL in output; all tests pass; PNG parser reports correct dimensions, CRCs, 300 DPI metadata, RGBA pixels, unique filenames, file size at or below 50 MiB, and no temporary files. diff --git a/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md b/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md new file mode 100644 index 0000000..dfd5439 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-hybrid-astar-p0-implementation.md @@ -0,0 +1,427 @@ +# Hybrid A* P0 规划核心 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。 + +**Goal:** 在既有 `PlanningGridMap` 之上提供可复用、可验证且不依赖 UI 的 Hybrid A* 粗路径规划服务。 + +**Architecture:** P0 先固定 m/rad/1/m 数据契约和连续车辆碰撞边界,再在该边界上实现恒曲率原语、二维启发式、确定性 Hybrid A*、路径重建和最终复核。`CoarsePathPlanningService` 是业务的唯一组合入口;`HybridAStarPlanner` 是只消费已建地图的下层门面。 + +**Tech Stack:** C# 10、.NET Standard 2.0、现有 `PlanningGridMap`、PowerShell 反射契约测试、`CancellationToken`。 + +## Global Constraints + +- 所有运行时代码位于 `ClumsyPilot/ParkrobTrajplanner/CoarsePath/`,命名空间为 `MultiWheelC.TrajectoryPlanning.CoarsePath` 或其子命名空间。 +- Map 只保存外部障碍物;安全余量只在连续车辆碰撞检查时扩张车辆矩形,绝不写入 Map。 +- 地图输入和障碍几何使用 mm;CoarsePath 的位置使用 m、航向使用 rad、曲率使用 1/m。 +- 规划器只能接受 `PlanningGridMap`,不得引用 TwoLeg、定位、Painter、UI 或系统时间。 +- 所有公开类型、构造函数、属性和方法使用中文 XML 文档,明确参数单位、边界以及返回或失败语义;内部几何/搜索不变量使用简短中文注释。 +- `netstandard2.0` 禁止直接使用 `PriorityQueue`、`Math.Clamp`、`double.IsFinite`、`record`、`init`。 +- 固定约束:原语最大长度 0.50 m;积分最大步长 0.05 m;碰撞中心步长不超过 `min(0.025 m, Map.ResolutionMeters / 2)`;默认终点容差为 0.15 m、5°。 +- 终点候选必须进入 Open List,只有作为最佳有效条目出队时才能成功;地图外始终按占据处理。 +- 本计划不实现 P1 的 Clumsy `MovementTest`、Painter 绘制、Release 性能基准或旧 TrapMap 入口退役。 +- 按用户现有约束,不执行 Git 自检、暂存、提交或推送。 + +--- + +## 文件结构 + +```text +ClumsyPilot/ParkrobTrajplanner/ +├── CoarsePath/ +│ ├── Contracts/ # 请求、结果、枚举和值对象 +│ ├── Vehicle/ # 扩大车辆几何和连续碰撞 +│ ├── Search/ # 原语、堆、启发式和 Hybrid A* 搜索 +│ ├── Output/ # 回溯、稠密路径装配和最终验证 +│ ├── Facade/ # 一次调用编排和调试旁路契约 +│ ├── HybridAStarPlanner.cs +│ └── README.md # 粗规划调用方文档;链接至 ../Map/README.md +└── tests/ + ├── verify_coarse_path_collision.ps1 + ├── verify_coarse_path_search.ps1 + └── verify_coarse_path_integration.ps1 +``` + +## Task 1: 固定公共契约、状态与默认配置 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs` +- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1` + +**Produces:** + +```csharp +public sealed class Pose2D +{ + public Pose2D(double xMeters, double yMeters, double headingRadians); + public double X { get; } + public double Y { get; } + public double Heading { get; } +} + +public sealed class PlanningRequest +{ + public PlanningGridMap Map { get; set; } + public Pose2D Start { get; set; } + public Pose2D Goal { get; set; } + public VehicleParameters Vehicle { get; set; } + public HybridAStarConfiguration Configuration { get; set; } + public double StartVehicleCurvature { get; set; } + public TravelDirection? StartDirection { get; set; } + public GoalDirectionConstraint GoalDirection { get; set; } +} +``` + +- [ ] **Step 1: 写失败的公共契约测试。** 在 `verify_planning_utils.ps1` 载入程序集后添加反射断言,检查 `Pose2D` 构造函数、三个枚举、`PlanningRequest` 属性和每个默认值。默认配置断言如下: + +```powershell +$config = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.HybridAStarConfiguration +Assert-Equal 0.50 $config.PrimitiveLengthMeters '原语最大长度' +Assert-Equal 0.05 $config.IntegrationStepMeters '积分步长' +Assert-Equal 0.025 $config.MaximumCollisionCheckStepMeters '碰撞步长' +Assert-Equal 5 $config.CurvatureLevelCount '曲率等级数' +Assert-Equal 200000 $config.MaximumExpandedNodes '节点上限' +``` + +- [ ] **Step 2: 运行测试并确认 RED。** + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +``` + +预期:脚本因 `CoarsePath` 类型尚不存在而以非零退出。 + +- [ ] **Step 3: 实现最小契约。** `TravelDirection` 仅含 `Forward`、`Reverse`;`GoalDirectionConstraint` 仅含 `Any`、`Forward`、`Reverse`;`CoarsePathPointSource` 仅含 `Start`、`MotionPrimitive`、`GoalTruncation`。`PlanningStatus` 必须包含 `Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed`、`InternalError`。 + +`HybridAStarConfiguration` 的构造默认值必须是:`Math.PI / 36d` 航向/终点航向容差、5 秒超时、`HeuristicWeight=1d`、`ReverseCostMultiplier=1.5d`、`GearSwitchPenaltyMeters=1d`、`CurvatureMagnitudeWeight=0.10d`、`CurvatureChangePenaltyMetersPerLevel=0.05d`、`ClearanceCostWeight=0.20d`、`ClearanceCostDistanceMeters=0.50d`。 + +`PlanningResult` 只允许成功结果携带非空路径与分段;所有失败工厂方法返回空只读集合并保留诊断。`PlanningDiagnostics` 固定记录扩展、生成、重开、陈旧堆条目、Open List 峰值、路径长度、最小保守净空、耗时和终止原因。 + +- [ ] **Step 4: 重新运行工具契约测试并确认 GREEN。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +``` + +预期:退出码为 0,既有 Utils/Map 契约仍可加载。 + +## Task 2: 实现扩大车辆足迹与连续碰撞检查 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs` +- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1` + +**Consumes:** `PlanningGridMap`、`Pose2D`、`VehicleParameters`。 + +**Produces:** + +```csharp +public sealed class FootprintCollisionChecker +{ + public bool IsPoseCollisionFree( + Pose2D pose, PlanningGridMap map, VehicleParameters vehicle, + double additionalMarginMeters, out double bodyClearanceMeters); + + public bool IsSweptMotionCollisionFree( + Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle, + double maximumCenterStepMeters, out double minimumBodyClearanceMeters); +} +``` + +- [ ] **Step 1: 写失败的连续碰撞测试。** 脚本通过 `PlanningMapFactory` 创建 50 mm 地图和单个薄矩形障碍,验证下面三种行为:车辆与障碍格擦边返回碰撞、距离场净空严格大于外接圆半径时返回安全、两个端点安全但中间穿过障碍时扫掠检查返回碰撞。 + +```powershell +$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker +$clearance = 0.0 +$safe = $checker.IsPoseCollisionFree($pose, $map, $vehicle, 0.0, [ref]$clearance) +Assert-False $safe '矩形擦边必须视为碰撞' +``` + +- [ ] **Step 2: 运行碰撞脚本并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +``` + +预期:因车辆命名空间和碰撞检查器不存在而失败。 + +- [ ] **Step 3: 实现最小连续几何。** `VehicleKinematics` 在最大曲率与最小转弯半径都存在时取 `Math.Min(maximumCurvature, 1d / minimumRadius)`。`VehicleFootprint` 用 `LengthMeters + 2 * SafetyMarginMeters` 和 `WidthMeters + 2 * SafetyMarginMeters` 构造以 `Pose2D` 为几何中心的旋转矩形、AABB 与外接圆。 + +`OrientedRectangleCellIntersection` 使用 SAT:矩形的两个单位轴和格子的世界 X/Y 轴都作为投影轴;任一轴存在严格分离才是不相交,投影接触算相交。`FootprintCollisionChecker` 依次验证四角均在地图内、用严格 `distance > radius + additionalMargin` 快速放行、遍历 AABB 内占据格并执行 SAT。扫掠检查将中心位移切分到 `min(maximumCenterStepMeters, map.ResolutionMeters / 2d)`,每一段的临时边距为 `0.5d * (centerDisplacement + circumscribedRadius * Math.Abs(headingDelta))`。 + +- [ ] **Step 4: 扩展碰撞测试并运行 GREEN。** 加入 0°、45°、任意航向、栅格中心/亚栅格中心、薄障碍、边界外和扫掠场景;随后运行: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +``` + +预期:构建与脚本退出码均为 0。 + +## Task 3: 实现原语积分、目标容差与内部截断 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs` +- Create: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**Consumes:** `Pose2D`、方向、车辆最大曲率、`HybridAStarConfiguration`、`FootprintCollisionChecker`。 + +**Produces:** 含方向、曲率、实际长度和内部积分点的不可变 `MotionPrimitive`;目标检查器只判定位置、航向和目标进入方向。 + +- [ ] **Step 1: 写失败的原语测试。** 验证直行、圆弧、倒车、0.50 m 上限、积分点间距上限,以及目标在 0.30 m 处时原语恰好截断到第一个满足条件的内部点。 + +```powershell +$primitive = $generator.Generate($start, $curvature, $direction, $config, $map) +Assert-True ($primitive.Points.Count -ge 1) '原语必须产生内部积分点' +Assert-Equal 0.30 $truncated.ActualLengthMeters '0.30m 目标必须在原语内部截断' +``` + +- [ ] **Step 2: 运行搜索脚本并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +``` + +预期:因原语类型尚不存在而失败。 + +- [ ] **Step 3: 实现解析积分和检查顺序。** 单步积分使用: + +```csharp +double signedDistance = direction == TravelDirection.Forward ? step : -step; +double nextHeading = AngleMath.NormalizeRadians(heading + curvature * signedDistance); +if (Math.Abs(curvature) < 1e-12) +{ + nextX = x + signedDistance * Math.Cos(heading); + nextY = y + signedDistance * Math.Sin(heading); +} +else +{ + nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature; + nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature; +} +``` + +原语点步长不得超过 `min(IntegrationStepMeters, MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2d)`。每个内部点严格按“有限数值 → 从前一点的扫掠碰撞 → 终点容差”执行;命中目标即截断,并标记 `GoalTruncation`。起点已满足目标时生成零长度终点候选,不生成运动原语。 + +- [ ] **Step 4: 运行原语测试并确认 GREEN。** 加入五个曲率等级、曲率相邻变化最多一级和 `±π` 航向容差的断言;再运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 + +## Task 4: 实现确定性 Open List、代价与二维启发式 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**Produces:** 内部二叉最小堆、等效米代价计算器和目标反向八邻域距离启发式。 + +- [ ] **Step 1: 写失败的堆、代价和启发式测试。** 验证堆的排序优先级为 `F`、`H`、较大 `G`、插入序号;验证八邻域斜向代价和禁止切过两个正交障碍的对角夹角;验证倒车、换向、曲率和净空代价项。 + +```powershell +Assert-Equal 'node-b' $heap.Pop().Id '相同 F 时应先选较小 H' +Assert-Throws { $calculator.Calculate($invalidInput) } '负权重必须拒绝' +Assert-True ([double]::IsPositiveInfinity($heuristic.GetCost($blockedRow, $blockedCol))) '二维不可达应为无穷' +``` + +- [ ] **Step 2: 运行搜索测试并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +``` + +预期:因堆、代价或启发式类型不存在而失败。 + +- [ ] **Step 3: 实现最小支持结构。** `BinaryMinHeap` 使用 `List`,比较器严格按 `F`、`H`、反向 `G`、插入序号。代价必须实现: + +```text +length * directionMultiplier * + (1 + curvatureMagnitudeWeight * abs(curvature / maximumCurvature) + + clearanceCostWeight * max(0, 1 - clearance / clearanceCostDistance)) ++ gearSwitchPenalty ++ curvatureChangePenalty * abs(curvatureLevelDelta) +``` + +`GridDijkstraHeuristic` 从目标格反向传播四邻域 1 倍格长和对角 `sqrt(2)` 倍格长;对角移动前确认两个正交邻格均未占据。 + +- [ ] **Step 4: 运行搜索测试并确认 GREEN。** 重复构造同一输入两次,断言出队顺序相同;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 + +## Task 5: 实现 Hybrid A* 节点、重开与终点候选管理 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` + +**Consumes:** Task 2–4 的碰撞、原语、堆、代价与启发式。 + +**Produces:** 接收已验证请求并返回成功节点索引或明确搜索失败状态的内部搜索器。 + +- [ ] **Step 1: 写失败的搜索测试。** 覆盖空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标方向、无解、取消、超时、节点上限、较小 `G` 重开和终点候选出队顺序。 + +```powershell +$result = $search.Search($request, [Threading.CancellationToken]::None) +Assert-Equal 'Success' $result.Status '空图应规划成功' +Assert-True $result.ReopenedNodeCount -gt 0 '更小 G 到达同键时必须允许重开' +``` + +- [ ] **Step 2: 运行搜索脚本并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +``` + +预期:因 `HybridAStarSearch` 不存在而失败。 + +- [ ] **Step 3: 实现离散键与搜索循环。** `HybridAStarNodeKey` 固定包含位置行列、航向索引、方向和曲率等级。普通状态的最佳 `G` 保存在 `Dictionary`;发现严格更小的 `G` 时压入新条目,旧条目在弹出时丢弃。循环在扩展前检查 `CancellationToken`、配置超时和最大扩展数。 + +终点候选压入同一 Open List,但不放入普通键的去重表;它只能在作为当前最佳有效条目弹出、重新验证终点条件与末段碰撞后成功。Open List 耗尽返回 `NoFeasiblePath`。 + +- [ ] **Step 4: 运行全量搜索场景并确认 GREEN。** 对每个固定场景重复运行两次并断言状态、路径代价和节点扩展顺序一致;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。 + +## Task 6: 回溯、路径装配、最终验证与下层门面 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` +- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Produces:** + +```csharp +public sealed class HybridAStarPlanner +{ + public PlanningResult Plan( + PlanningRequest request, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] **Step 1: 写失败的路径输出测试。** 验证首点弧长为 0、弧长不递减、展开航向连续、终点截断来源、相邻重复点只允许作为换向对、分段的包含式索引覆盖全部路径。 + +```powershell +Assert-Equal 0.0 $result.Path[0].ArcLength '起点弧长必须为零' +Assert-True ($result.Segments[-1].EndIndex -eq ($result.Path.Count - 1)) '分段必须覆盖尾点' +Assert-Equal 'FinalValidationFailed' $invalid.Status '最终复核失败不能发布部分路径' +``` + +- [ ] **Step 2: 运行集成脚本并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:因 `HybridAStarPlanner` 与输出类型不存在而失败。 + +- [ ] **Step 3: 实现回溯和最终复核。** 搜索节点只保留父索引和原语描述;`PathBacktracker` 在成功后使用同一解析积分公式重建内部点。`CoarsePathAssembler` 累计弧长,保持换向处两个相同位姿/弧长而方向不同的点,并让新方向点设置 `IsGearSwitchPoint=true`。 + +`CoarsePathValidator` 使用与搜索相同的 `FootprintCollisionChecker` 和扫掠规则,检查有限数、曲率上限、起终点容差/方向、弧长单调性、换向对与分段覆盖。验证失败返回 `FinalValidationFailed`,路径和分段均为空。 + +`HybridAStarPlanner` 在调用搜索前映射空请求、Map 未就绪、车辆无效、曲率配置无效、起终点越界及起终点碰撞;其余异常收敛为 `InternalError` 并记录诊断。 + +- [ ] **Step 4: 运行碰撞、搜索和集成脚本并确认 GREEN。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:三个脚本均退出 0。 + +## Task 7: 一次调用服务、调试旁路契约与 README + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Produces:** + +```csharp +public sealed class CoarsePathPlanningService +{ + public CoarsePathPlanningJobResult Plan( + CoarsePathPlanningJob job, + CancellationToken cancellationToken = default(CancellationToken)); +} +``` + +- [ ] **Step 1: 写失败的一次调用测试。** 断言服务先建图、地图失败时不搜索、成功时同时返回 `PlanningMapBuildResult` 和 `PlanningResult`;同一服务实例两次使用相同地图请求时第二次是 `Input` 缓存命中。 + +```powershell +$service = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Facade.CoarsePathPlanningService +$first = $service.Plan($job, [Threading.CancellationToken]::None) +$second = $service.Plan($job, [Threading.CancellationToken]::None) +Assert-Equal 'Input' $second.MapResult.CacheHit.ToString() '服务必须长期持有地图工厂' +``` + +- [ ] **Step 2: 运行集成脚本并确认 RED。** + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:因门面类型与 README 不存在而失败。 + +- [ ] **Step 3: 实现服务和文档。** `CoarsePathPlanningService` 构造时创建一个长期 `PlanningMapFactory` 与一个 `HybridAStarPlanner`,计划调用顺序固定为: + +```text +PlanningMapFactory.Create(job.MapRequest) + -> 地图失败:包装 MapResult,返回空 PlanningResult + -> 地图成功:HybridAStarPlanner.Plan(job 转换的 PlanningRequest) + -> 仅依 Debug 选项向 IPlanningDebugSink 发布旁路数据 +``` + +默认 sink 为空实现。任何 sink 异常只追加调试诊断,绝不改变地图哈希、规划状态、路径或分段。 + +README 必须包含以下小节:模块范围;Map 与 CoarsePath 的职责表;`CoarsePathPlanningService.Plan` 的可编译调用示例;mm/m/rad/1/m 单位表;`SourceVersion` 与缓存规则;`PlanningStatus` 处理示例;路径点和方向分段含义;第一版不支持的平滑、速度规划、控制和横移能力;到 `../Map/README.md` 的链接。 + +- [ ] **Step 4: 运行完整 P0 验收。** + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +预期:构建和全部七个脚本退出码均为 0;不执行 P1 UI 或性能工作。 + +## Plan Self-Review + +- 覆盖性:Task 1 覆盖公共契约;Task 2 覆盖连续车辆碰撞;Task 3–5 覆盖原语、代价、启发式、确定性搜索与终点候选;Task 6 覆盖输出与最终复核;Task 7 覆盖一次调用门面、文档和全量验收。 +- 类型一致性:所有搜索和门面输入均以 `PlanningGridMap`、`PlanningRequest`、`CoarsePathPlanningJob` 为唯一跨层契约;Map 构建只存在于 Task 7 的服务门面。 +- 范围:没有包含 Clumsy UI、Painter、性能基准或旧 TrapMap 迁移,这些均为 P1。 diff --git a/docs/superpowers/plans/2026-07-27-map-documentation.md b/docs/superpowers/plans/2026-07-27-map-documentation.md new file mode 100644 index 0000000..a09435e --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-map-documentation.md @@ -0,0 +1,242 @@ +# Map 模块文档与注释实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 Map 模块提供根目录结构说明,并为所有公共 API 提供中文、Python docstring 风格的调用说明。 + +**Architecture:** `Map/README.md` 只说明模块结构、数据流、单位与入口;`.cs` 中的 `/// ` 是参数、返回和约束的唯一 API 文档来源。注释不得改变方法签名、建图算法、缓存键或任何运行时行为。 + +**Tech Stack:** C# 10、netstandard2.0、PowerShell 验证脚本、Markdown。 + +## Global Constraints + +- 所有新增说明使用中文。 +- 公共 API 文档使用可被 C# IDE 识别的 `///`,内容顺序为“功能、参数、返回、注意”。 +- 参数说明必须给出单位、坐标系、可空性或输入约束中的适用项。 +- 返回说明必须给出结果数据的业务意义;`bool` 说明其 true/false 语义。 +- README 不复制逐个属性的完整参数表。 +- 不修改运行逻辑,不执行 Git 自检、暂存、提交或重置。 + +--- + +### Task 1: 建立 Map README 与文档存在性检查 + +**Files:** +- Create: `ClumsyPilot/ParkrobTrajplanner/Map/README.md` +- Create: `ClumsyPilot/tests/verify_planning_map_documentation.ps1` + +**Interfaces:** +- Consumes: `PlanningMapFactory.Create(PlanningMapRequest request)`、Map 现有目录结构。 +- Produces: Map 模块入口说明和可重复运行的文档检查。 + +- [ ] **Step 1: 写入失败检查** + +创建 PowerShell 脚本,读取 `Map/README.md`,断言它不存在时抛出异常;创建后继续断言包含以下固定标题:`# Map 模块说明`、`## 文件结构`、`## 建图数据流`、`## 坐标与单位`、`## 最小调用示例`、`## 缓存与版本`、`## 测试与调试`。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 因 `Map/README.md` 不存在而失败。 + +- [ ] **Step 3: 创建 README** + +写入当前 `Core`、`Obstacles`、`Sources`、`Planning`、`Test`、`Test/Visualization` 的目录树;每个 `.cs` 文件后写一句职责。说明数据流为 `PlanningMapRequest → IMapObstacleSource → EnvironmentMapBuilder/MapObstacleRasterizer → EnvironmentGridMap → PlanningMapAdapter/ObstacleDistanceField → PlanningGridMap`。说明环境图使用世界 mm、规划查询使用 m、范围采用左闭右开;示例只经长期持有的 `PlanningMapFactory.Create` 调用;说明 `SourceVersion` 变化与两级缓存的关系;明确 PNG 为可选调试、旧 TrapMap 不属于新运行时入口。 + +- [ ] **Step 4: 运行检查确认通过** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: `Planning map documentation checks passed.` + +### Task 2: 注释公共建图入口与结果契约 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs` + +**Interfaces:** +- Consumes: 外部调用者提供的地图范围、分辨率、障碍物来源。 +- Produces: 建图请求、构建结果、缓存命中状态的中文 API 契约。 + +- [ ] **Step 1: 扩展失败检查** + +在 `verify_planning_map_documentation.ps1` 中对上述文件断言:`PlanningMapFactory.Create`、`PlanningMapRequest.Bounds`、`PlanningMapBuildResult.Map` 前方紧邻中文 `///` 注释,且包含 `参数:`、`返回:`、`单位:` 或 `注意:` 中适用的说明。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 失败并指出缺失的入口契约说明。 + +- [ ] **Step 3: 添加入口契约注释** + +为 `PlanningMapFactory`、构造和 `Create` 写明长期复用要求、请求输入、结果与三种缓存命中语义。为请求与结果的每个公共属性写明单位、可空性和失败/空图语义。为 `PlanningMapCacheHit` 的每个枚举值写明 `None`、`Input`、`Occupancy` 的实际含义。为内部 Map 构建请求和结果的 public 成员补充相同层级说明。 + +- [ ] **Step 4: 运行入口检查与编译** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` + +Expected: 文档检查通过;编译 0 error。 + +### Task 3: 注释地图边界、环境栅格与障碍物契约 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs` + +**Interfaces:** +- Consumes: 世界坐标毫米几何、有效栅格范围。 +- Produces: 环境占据图以及几何到栅格的公开行为说明。 + +- [ ] **Step 1: 扩展失败检查** + +对 `MapBoundsMm` 构造函数、`Contains`、`GetDimensions`,`EnvironmentGridMap` 构造函数和世界/栅格查询方法,以及两种障碍物构造函数与属性,断言有中文 `///`。脚本还断言 `MapObstacleRasterizer.Rasterize` 注释包含其是唯一写栅格入口的约束。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 失败并显示尚未文档化的公共几何/栅格 API。 + +- [ ] **Step 3: 添加边界与几何注释** + +为范围、行列、世界 mm 坐标、左闭右开边界、越界 `false`/占据行为、`out row/col` 的失败值写明说明。为圆和矩形的坐标、半径与 `IsValid` 写明单位和 true/false 条件。为环境构建器 `Build` 写明必需来源失败会整体失败、可选来源只记录状态的规则。 + +- [ ] **Step 4: 运行检查与 Map 适配器脚本** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1` + +Expected: 两个脚本通过。 + +### Task 4: 注释障碍物来源与 TwoLeg 投影契约 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs` + +**Interfaces:** +- Consumes: 纯检测快照和外部障碍物几何。 +- Produces: 世界 mm 几何、来源状态和诊断信息。 + +- [ ] **Step 1: 扩展失败检查** + +对来源接口的 ID、版本、必需性和 `ProjectToWorld`,TwoLeg 输入构造函数/属性,以及投影结果工厂方法和状态枚举值断言中文 API 说明。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 失败并指出缺失的来源或 TwoLeg 契约说明。 + +- [ ] **Step 3: 添加来源注释** + +明确 `ProjectToWorld` 不得读传感器、定位、UI、时钟;`SourceVersion` 必须在快照内容变化时递增;`IsRequired` 的失败语义;TwoLeg 检测时世界位姿和两腿局部 mm 坐标、航向弧度、半径单位;`Applied/Empty/Unavailable/Invalid` 的规划含义。 + +- [ ] **Step 4: 运行来源工厂验证** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1` + +Expected: `Planning map factory checks passed.` + +### Task 5: 注释规划快照、距离场与缓存契约 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapCache.cs` + +**Interfaces:** +- Consumes: 环境占据栅格和建图输入/占据哈希。 +- Produces: 不可变规划快照、保守距离和缓存复用行为说明。 + +- [ ] **Step 1: 扩展失败检查** + +断言 `PlanningGridMap` 的公共属性与查询方法、适配器/距离场/EDT 的公共静态方法、缓存公共方法均有中文 `///`;检查 `PlanningGridMap` 注释含 m 与 mm 的单位区分。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 失败并报告缺失的规划或缓存说明。 + +- [ ] **Step 3: 添加规划与缓存注释** + +说明 `PlanningGridMap` 不可变、规划世界查询使用 m、越界视为占据/零净距、距离是保守下界;说明适配器从 mm 环境图转为 m 规划图;说明 EDT 输出平方距离;说明缓存容量为四、输入命中返回同一快照、占据命中共享数组但颁发新快照元数据。 + +- [ ] **Step 4: 运行适配器和工厂验证** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1` + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1` + +Expected: 两个脚本通过。 + +### Task 6: 注释测试、PNG 调试公共 API并完成总验证 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs` + +**Interfaces:** +- Consumes: 只读 `PlanningGridMap` 和可选 PNG 输出目录。 +- Produces: 清晰的 MapTest 配置/日志语义和 PNG 导出结果说明。 + +- [ ] **Step 1: 扩展失败检查** + +对 `PlanningMapTest.Test/TestStop`、PNG 请求/结果的每个属性、导出器常量与 `ExportIfEnabled`、渲染器和 PNG 写入器公共方法断言中文 `///`。 + +- [ ] **Step 2: 运行检查确认失败** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1` + +Expected: 失败并列出测试或可视化公共成员。 + +- [ ] **Step 3: 添加测试与 PNG 注释** + +说明 MapTest 是手工 Clumsy 入口,日志/PNG 开关仅影响调试;说明 PNG 不参与建图和缓存;说明输出目录、像素尺寸、字节大小、Saved/Skipped 的语义;说明 `ValidatedPngWriter.Write` 输入是 RGBA 行主序字节及其宽高。 + +- [ ] **Step 4: 完整验证** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 +``` + +Expected: 所有脚本通过;编译 0 error。现有过时 API 警告若仍来自 `MovementTests.TireFollowing.cs` 或 `TireFollowing.cs`,记录为非本任务引入。 + +## Plan Self-Review + +- Spec coverage: Task 1 覆盖 README 和目录结构;Task 2 至 Task 6 覆盖全部 public API 分层;Task 6 覆盖完整验证。 +- Placeholder scan: 本计划没有 TODO、TBD 或未指定的验证命令。 +- Type consistency: 文中使用的类型和方法名均来自当前 Map 源码;不引入新运行时接口。 diff --git a/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md b/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md new file mode 100644 index 0000000..3b5a31a --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md @@ -0,0 +1,534 @@ +# P1 粗路径 Clumsy UI 集成 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 交付可在 Clumsy 中后台运行的七个粗路径测试入口,显示真实规划栅格快照和完整路径信息,并支持传入 AMR 世界位姿与手动目标位姿。 + +**Architecture:** `CoarsePathScenarioFactory` 保持无 UI 的纯输入构造职责,提供六个可重复的回归场景及一个显式标注为空图演示的“AMR 位姿 + 手动终点”请求创建入口。`MovementTest.CoarsePathTest.cs` 只作为 UI 适配层:把 AMR/目标 mm+deg 转为核心所需的 m+rad,使用一个共享门面在 `Task.Run` 后台运行,并从不可变 `CoarsePathPlanningJobResult` 绘制地图快照和路径。 + +**Tech Stack:** C# / `netstandard2.0`、现有 Clumsy `MovementTest`/`Painter`、`CoarsePathPlanningService`、PowerShell 反射验证脚本。 + +## Global Constraints + +- `PlanningMapRequest` 的地图、障碍物、AMR 输入和手动目标 X/Y 均为世界 mm;`Pose2D` 和路径 X/Y 为世界 m;核心航向为 rad。 +- 项目上游的 AMR `th` 输入按 deg 适配为 `th * Math.PI / 180d`;不得沿用直接将该值传给 `Math.Cos/Sin` 的旧写法。 +- AMR 起点必须是车辆几何中心;传感器安装点必须由上游先按外参转换。 +- UI 和测试只能调用 `CoarsePathPlanningService.Plan(job, token)`;不得直接实例化 `PlanningMapFactory`、`HybridAStarPlanner`、栅格化器、碰撞器、原语生成器或搜索节点。 +- 所有七个 MovementTest 都不得引用 `BasicPilotBase.Chassis`、`SendMotion`、`DriveTask` 或任何底盘控制 API。 +- `Test` 不得等待后台任务或读取 `Task.Result`;`TestStop` 先取消令牌,再使运行编号失效、解绑任务并清空 Painter。 +- 只在 `PlanningStatus.Success` 绘制路径、方向箭头、换向点和扩大车体检查框;失败、取消和超时只显示地图、起点、终点和状态。 +- 代码兼容 `netstandard2.0`,不引入新 NuGet 包;公开类型/成员写中文 XML 文档,复杂单位与并发逻辑写简短中文行注释。 +- 不改动 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。 + +--- + +## 文件结构 + +| 文件 | 变更职责 | +| --- | --- | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新建纯场景工厂、六场景枚举、AMR/目标位姿 mm+deg 到核心 `Pose2D` 的转换,以及空图演示手动目标请求。 | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 新建七个 UI 入口、共享会话执行器、任务取消、Painter 地图/路径/图例绘制与手动输入解析。 | +| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 为工厂行为、单位转换、缓存/换向/无解、UI 源码边界与 README 内容新增真实程序集和文本断言。 | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 记录 P1 测试入口、输入单位、空图演示限制、图例、停止语义及无底盘命令边界。 | + +### 固定接口 + +```csharp +namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; + +public enum CoarsePathTestScenario +{ + ExplicitEmpty, + RectangleDetour, + ManualAndTwoLeg, + CacheHit, + ReverseGearSwitch, + NoFeasiblePath, +} + +public static class CoarsePathScenarioFactory +{ + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario); + + public static CoarsePathPlanningJob CreateManualGoalDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees); +} +``` + +`CreateManualGoalDemo` 只构造带 2,000 mm 边缘留白的显式空图演示请求,并在 README/测试名称中明确其不代表真实环境安全。未来现场入口必须提供真实 `IMapObstacleSource` 快照,而不是修改此方法的语义。 + +### Task 1: 工厂契约与失败测试 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` +- Create later in Task 2: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` + +**Consumes:** 已有 `CoarsePathPlanningService.Plan(CoarsePathPlanningJob, CancellationToken)`、`Find-Method`、`Assert-True`、`Assert-Equal` 与程序集加载逻辑。 + +**Produces:** 对 `CoarsePathTestScenario` 和 `CoarsePathScenarioFactory` 的反射行为约束;Task 2 的最小实现必须使这些断言通过。 + +- [ ] **Step 1: 在集成脚本加入工厂反射测试** + +在现有 facade 检查后、最终输出前插入以下 PowerShell。它要求类型和两个公开方法都存在,因此在工厂未创建时失败。 + +```powershell +$testNamespace = $coarsePath + 'Test.' +$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) +$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) +Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' +Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' + +$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) +$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( + [double], [double], [double], [double], [double], [double]) +Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' +Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' + +$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') +foreach ($scenarioName in $scenarioNames) { + $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) + $jobA = $factoryCreate.Invoke($null, @($scenario)) + $jobB = $factoryCreate.Invoke($null, @($scenario)) + Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." + Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." +} + +$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) +Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' +Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' +Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' +Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' +Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' +Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' +``` + +- [ ] **Step 2: 运行脚本确认失败** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建成功;脚本因 `P1 scenario enum must exist.` 失败。 + +### Task 2: 实现纯场景工厂并通过行为测试 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` +- Test: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Task 1 的枚举和两个公开工厂方法;`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource`、`Pose2D`、`VehicleParameters`、`HybridAStarConfiguration`。 + +**Produces:** 六个可重复 job 与一个空图演示手动 job,供 UI 入口和后续脚本行为断言共同使用。 + +- [ ] **Step 1: 先建立最小的公共类型和转换辅助函数** + +创建工厂文件并定义以下枚举、转换函数和公共入口。所有输入先做有限值检查;非有限输入抛出 `ArgumentOutOfRangeException`,避免伪造核心请求。 + +```csharp +public enum CoarsePathTestScenario +{ + ExplicitEmpty, + RectangleDetour, + ManualAndTwoLeg, + CacheHit, + ReverseGearSwitch, + NoFeasiblePath, +} + +public static class CoarsePathScenarioFactory +{ + private const double MillimetersPerMeter = 1000d; + private const double DegreesToRadians = Math.PI / 180d; + private const float ResolutionMillimeters = 50f; + private const double ManualMapPaddingMillimeters = 2000d; + + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) + { + switch (scenario) + { + case CoarsePathTestScenario.ExplicitEmpty: return CreateExplicitEmpty(); + case CoarsePathTestScenario.RectangleDetour: return CreateRectangleDetour(); + case CoarsePathTestScenario.ManualAndTwoLeg: return CreateManualAndTwoLeg(); + case CoarsePathTestScenario.CacheHit: return CreateRectangleDetour(); + case CoarsePathTestScenario.ReverseGearSwitch: return CreateReverseGearSwitch(); + case CoarsePathTestScenario.NoFeasiblePath: return CreateNoFeasiblePath(); + default: throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + + public static CoarsePathPlanningJob CreateManualGoalDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) + { + EnsureFinite(startXMillimeters, nameof(startXMillimeters)); + EnsureFinite(startYMillimeters, nameof(startYMillimeters)); + EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); + EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); + EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); + EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); + return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters), + ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), + ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); + } + + private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) + => new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, + headingDegrees * DegreesToRadians); +} +``` + +- [ ] **Step 2: 实现统一请求模板和六个固定场景** + +使用统一的车辆和配置,避免场景间无意改变安全或搜索语义。模板必须是新对象: + +```csharp +private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, + TravelDirection? startDirection, GoalDirectionConstraint goalDirection) +{ + return new CoarsePathPlanningJob + { + MapRequest = mapRequest, + Start = start, + Goal = goal, + Vehicle = new VehicleParameters + { + LengthMeters = 0.80d, + WidthMeters = 0.60d, + SafetyMarginMeters = 0.05d, + MaximumCurvaturePerMeter = 1d / 1.20d, + }, + Configuration = new HybridAStarConfiguration(), + StartDirection = startDirection, + GoalDirection = goalDirection, + }; +} +``` + +固定地图均使用 `new MapBoundsMm(0f, 6000f, 0f, 4000f)`、50 mm 分辨率。以下代码固定各场景的障碍来源和世界位姿,所有 `ManualObstacleSource` 版本为 `1L`、所有必需来源为 `true`: + +```csharp +private static CoarsePathPlanningJob CreateExplicitEmpty() + => CreateJob(CreateMap(true, Array.Empty()), + new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); + +private static CoarsePathPlanningJob CreateRectangleDetour() + => CreateJob(CreateMap(false, new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] + { new AxisAlignedRectangleObstacle(2700f, 3300f, 1200f, 2800f) }), + }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); + +private static CoarsePathPlanningJob CreateManualAndTwoLeg() + => CreateJob(CreateMap(false, new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] + { + new CircleObstacle(2400f, 1300f, 220f), + new AxisAlignedRectangleObstacle(3000f, 3600f, 2000f, 2600f), + }), + new TwoLegObstacleSource("two-leg", 1L, true, + new TwoLegProjectionInput(true, 3900f, 2500f, 0d, + -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), + }), new Pose2D(1d, 1d, 0d), new Pose2D(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); + +private static CoarsePathPlanningJob CreateNoFeasiblePath() + => CreateJob(CreateMap(false, new IMapObstacleSource[] + { + new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] + { new AxisAlignedRectangleObstacle(2900f, 3100f, 0f, 4000f) }), + }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); +``` + +`CreateMap` 返回新的 `PlanningMapRequest`,固定写入地图边界、分辨率、给定来源和 `AllowExplicitEmptyMap`。倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。 + +倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。若 P0 的离散搜索在此几何下无法稳定得到成功,只允许调整此场景的目标距离或障碍布局,且测试必须继续要求 `IsGearSwitchPoint=true`。 + +`CreateManualDemoMap` 用起终点 X/Y 的最小/最大值各扩展 `ManualMapPaddingMillimeters`,按 50 mm 向外取整,并明确设置 `AllowExplicitEmptyMap=true` 和空的 `ObstacleSources`。 + +- [ ] **Step 3: 扩展行为断言以覆盖所有场景的实际状态** + +在 Task 1 的反射代码之后增加服务执行测试。它不引用任何 Painter 或 MovementTest: + +```powershell +$scenarioService = [Activator]::CreateInstance($serviceType) +foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { + $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) + $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) + Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." +} + +$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) +$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) +Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' + +$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) +$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) +Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' +Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' + +$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) +$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) +$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) +$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) +Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' +Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' +``` + +- [ ] **Step 4: 运行测试并固定数值场景** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 工厂、单位转换、六个固定场景、缓存与无解断言通过;此时尚未加入 MovementTest 源码检查,因此脚本整体通过。 + +### Task 3: MovementTest 后台会话与完整 Painter 绘制 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` +- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` + +**Consumes:** Task 2 的 `CoarsePathScenarioFactory.Create` 和 `CreateManualGoalDemo`;`CoarsePathPlanningService`、`CoarsePathPlanningJobResult`、`PlanningGridMap`、`Painter`。 + +**Produces:** 六个固定场景入口与一个“AMR 位姿 + 手动终点(空图演示)”入口;所有入口通过同一个后台执行器运行和绘制。 + +- [ ] **Step 1: 写入失败的 UI 源码结构断言** + +在 PowerShell 脚本中加入下列纯文本检查,避免在自动化测试中实例化外部 UI: + +```powershell +$movementTestPath = Join-Path $plannerRoot 'CoarsePath\Test\MovementTest.CoarsePathTest.cs' +if (-not (Test-Path -LiteralPath $movementTestPath -PathType Leaf)) { + throw 'P1 coarse-path MovementTest source file must exist.' +} +$movementTestContent = Get-Content -LiteralPath $movementTestPath -Raw +foreach ($required in @( + '[MovementTest(name = "粗路径-显式空图")]', + '[MovementTest(name = "粗路径-矩形绕行")]', + '[MovementTest(name = "粗路径-多来源障碍")]', + '[MovementTest(name = "粗路径-缓存命中")]', + '[MovementTest(name = "粗路径-倒车换向")]', + '[MovementTest(name = "粗路径-无解")]', + '[MovementTest(name = "粗路径-AMR起点手动终点(空图演示)")]', + 'Task.Run', 'CancellationTokenSource', 'CoarsePathPlanningService', + 'PlanningGridMap', 'IsOccupied', 'ResolutionMm', 'SnapshotId', '图例', 'IsGearSwitchPoint')) { + Assert-True $movementTestContent.Contains($required) "MovementTest must contain: $required" +} +foreach ($forbidden in @('PlanningMapFactory', 'HybridAStarPlanner', 'MapObstacleRasterizer', + 'FootprintCollisionChecker', 'MotionPrimitiveGenerator', 'HybridAStarSearch', + 'BasicPilotBase.Chassis', 'SendMotion', 'DriveTask', '.Wait()', '.Result')) { + Assert-False $movementTestContent.Contains($forbidden) "MovementTest must not depend on: $forbidden" +} +``` + +- [ ] **Step 2: 运行脚本确认 UI 结构检查失败** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 在工厂断言通过后,因 `P1 coarse-path MovementTest source file must exist.` 失败。 + +- [ ] **Step 3: 实现共享会话执行器与七个薄入口** + +在新文件中使用 `namespace MultiWheelC;`,引用 `System.Threading`、`System.Threading.Tasks`、`System.Drawing`、`System.Numerics`、`ClumsyCore`、`MDCSToolBox.Clumsy.Movements` 和 Map/CoarsePath 命名空间。定义一个内部静态执行器,核心形状如下: + +```csharp +internal static class CoarsePathMovementTestRunner +{ + private static readonly object SyncRoot = new object(); + private static readonly CoarsePathPlanningService Service = new CoarsePathPlanningService(); + private static readonly Painter Painter = UI.GetPainter("CoarsePathPlanningV1", true); + private static long _nextRunId; + private static long _activeRunId; + private static CancellationTokenSource _activeCancellation; + private static Task _activeTask; + + internal static void Start(string displayName, CoarsePathPlanningJob job) + { + CancellationTokenSource previous; + long runId; + var cancellation = new CancellationTokenSource(); + lock (SyncRoot) + { + previous = _activeCancellation; + runId = ++_nextRunId; + _activeRunId = runId; + _activeCancellation = cancellation; + Painter.Clear(); + _activeTask = Task.Run(() => Service.Plan(job, cancellation.Token)); + _activeTask.ContinueWith(task => Complete(runId, displayName, job, cancellation, task), + CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + } + previous?.Cancel(); + } + + internal static void Stop() + { + CancellationTokenSource cancellation; + lock (SyncRoot) + { + cancellation = _activeCancellation; + } + cancellation?.Cancel(); + lock (SyncRoot) + { + if (!ReferenceEquals(_activeCancellation, cancellation)) return; + _activeCancellation = null; + _activeTask = null; + _activeRunId = ++_nextRunId; + } + Painter.Clear(); + } +} +``` + +`Complete` 必须捕获 `task.Exception`,但正常情况下只接受 `CoarsePathPlanningJobResult`。在锁内确认 `runId == _activeRunId`、任务未取消并且 `task.Status == TaskStatus.RanToCompletion` 后才绘制;无论绘制与否都在 finally 中释放该任务专用 `CancellationTokenSource`。不可在锁内等待任务。 + +添加一个抽象 `CoarsePathScenarioMovementTest`,其 `Test` 调用 `CoarsePathMovementTestRunner.Start(DisplayName, CoarsePathScenarioFactory.Create(Scenario))`,其 `TestStop` 调用 `Stop()`。实现六个带固定属性名称的密封子类。第七个类在 `Test` 中只读取一次六个 UI 输入:AMR 起点 X/Y/航向和目标 X/Y/航向(分别为 mm/mm/deg),调用 `CreateManualGoalDemo` 后启动;输入解析失败时仅记录错误并不启动任务。 + +- [ ] **Step 4: 实现确定的地图和结果绘制辅助方法** + +在同一执行器内只消费 `job` 与 `CoarsePathPlanningJobResult`,按固定顺序调用以下辅助方法: + +```csharp +private static void DrawMap(PlanningGridMap map); +private static void DrawPose(Color color, string label, Pose2D pose); +private static void DrawGoalTolerance(Pose2D goal, HybridAStarConfiguration configuration); +private static void DrawSuccessfulPath(PlanningResult result, VehicleParameters vehicle); +private static void DrawLegendAndStatus(string displayName, CoarsePathPlanningJobResult result, int gridStride); +``` + +`DrawMap` 画 `[Bounds.XMin, Bounds.XMax) × [Bounds.YMin, Bounds.YMax)` 的粗边界和 X/Y 参考。`gridStride = Max(1, Ceiling(Max(Rows, Cols) / 100d))`;每 `gridStride` 个真实栅格画一条线,状态文字写入 `分辨率=...mm,显示每...格`。遍历 `row/col`,仅对 `map.IsOccupied(row,col)` 为 true 的单元以四条边线画深色格框,确保显示的是最终快照而非原始几何。 + +`DrawPose` 将 m 转 mm,以圆、朝向短线和标签分别绘制绿色起点、橙色终点。`DrawGoalTolerance` 将位置容差 m 转 mm,绘制橙色容差圆。`DrawSuccessfulPath` 仅在 `result.Status == PlanningStatus.Success` 时运行:相邻路径点按当前点 `Direction` 使用青色(前进)或蓝色(倒车)连线;每隔 10 点画短箭头;`IsGearSwitchPoint` 画紫色圆与“换向”;首、末、换向和每 20 点调用旋转矩形绘制,半长/半宽严格按车辆长宽加安全余量。`DrawLegendAndStatus` 在边界左上方显示边界、占据格、起点、终点、前进、倒车、换向和扩大车体颜色说明,另显示 `SnapshotId`、`MapResult.Status`、`CacheHit`、`PlanningResult.Status`、`Elapsed` 和终止原因。 + +- [ ] **Step 5: 运行构建与集成脚本** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建成功;工厂行为、七入口结构、后台取消约束和 Painter 数据来源检查全部通过。 + +### Task 4: README 与文档断言 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Task 2 的 `CreateManualGoalDemo` 单位契约和 Task 3 的七个入口名称、图例颜色和停止行为。 + +**Produces:** 可独立使用的 P1 UI 说明,以及对其关键安全声明的自动化保护。 + +- [ ] **Step 1: 为 README 写失败断言** + +在现有 README 检查后加入: + +```powershell +foreach ($requiredReadmeText in @( + '## P1:Clumsy 手动测试与可视化', + '粗路径-AMR起点手动终点(空图演示)', + 'AMR 位姿输入:X/Y 使用世界 mm,th 使用 deg', + 'Pose2D:X/Y 使用 m,航向使用 rad', + '显式空图只能用于演示', + '不会发送底盘运动命令', + 'TestStop', + '栅格边界', + '占据格', + '换向')) { + Assert-True $coarsePathReadmeContent.Contains($requiredReadmeText) "CoarsePath README must document: $requiredReadmeText" +} +``` + +- [ ] **Step 2: 运行脚本确认 README 检查失败** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 所有代码检查通过;脚本因 `CoarsePath README must document: ## P1:Clumsy 手动测试与可视化` 失败。 + +- [ ] **Step 3: 在 README 增加 P1 专节** + +在“第一版限制”之前增加 `## P1:Clumsy 手动测试与可视化`,逐项写明: + +1. 七个 MovementTest 名称及对应场景;缓存测试连续运行两次,第二次展示 `Input` 命中。 +2. AMR/手动目标输入契约:世界 `X/Y(mm)`、`th(deg)`,转换成 `Pose2D` 的 m/rad;起点是车辆几何中心。 +3. 空图手动目标入口只能演示坐标、路径和取消流程;现场必须提供真实障碍物快照。 +4. 可视化图例:边界、抽稀格线、占据格、起点、终点及容差、前进、倒车、换向和扩大车体检查框;失败不显示部分路径。 +5. `Test` 在后台规划,`TestStop` 取消令牌并清空图层;测试只显示结果,绝不发送底盘运动命令或执行路径跟踪。 + +- [ ] **Step 4: 运行 README 与集成检查** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建成功;README 和全部 P1 集成检查通过。 + +### Task 5: 全量回归与手动核验说明 + +**Files:** + +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Tasks 1–4 的代码、文档和脚本。 + +**Produces:** 通过 Debug 回归的 P1 UI 集成首个交付;不进入 Release 性能基准。 + +- [ ] **Step 1: 执行 Debug 构建和所有现存 P0/P1 功能脚本** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建 0 errors;每个存在的脚本返回 0 并输出其 `passed` 消息。 + +- [ ] **Step 2: 手动 Clumsy 验收** + +在 Clumsy 的 MovementTest 列表依次运行“粗路径-矩形绕行”和“粗路径-AMR起点手动终点(空图演示)”。检查:测试启动后界面仍可操作;图层拥有边界、格线、占据格、图例、起终点与状态;成功案例有方向区分路径和扩大车体框;点击停止后图层清空且没有任何底盘运动命令。 + +- [ ] **Step 3: 记录交付边界** + +在完成报告中明确:P1 UI 集成已完成;下一 P1 子项目是 Release 性能、资源和确定性基准;TrapMap 迁移/清理继续排除;没有执行 Git 操作。 + +## 自检 + +- 覆盖性:Task 2 交付纯场景与单位转换;Task 3 交付后台七入口和完整视觉要素;Task 4 交付 README;Task 5 交付自动化与手动验收。规格中的空图限制、实际占据快照、停止语义、无底盘命令和不显示部分路径均有对应任务。 +- 占位符:已检查任务不含未决占位、延后实现或泛化错误处理类措辞;每个实现任务均给出文件、接口、测试、命令和具体代码形状。 +- 类型一致性:所有任务统一使用 `CoarsePathTestScenario`、`CoarsePathScenarioFactory.Create`、`CreateManualGoalDemo`、`CoarsePathPlanningJob`、`CoarsePathPlanningJobResult` 和 `PlanningGridMap`;AMR 输入始终是 mm+deg,核心位姿始终是 m+rad。 diff --git a/docs/superpowers/plans/2026-07-27-planning-operation-budget.md b/docs/superpowers/plans/2026-07-27-planning-operation-budget.md new file mode 100644 index 0000000..f58ccc6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-planning-operation-budget.md @@ -0,0 +1,369 @@ +# 规划操作预算与诊断收尾实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让一次粗规划调用在建图、距离场、Dijkstra 和 Hybrid A* 阶段共用可取消的总超时预算,并发布真实的 Open List 诊断计数。 + +**Architecture:** 在 `Utils` 新增内部 `PlanningOperationBudget`,它不依赖 Map 或 CoarsePath,只报告继续、取消、超时。Map 与搜索分别将该中立结果映射为自己的结果;`CoarsePathPlanningService` 创建唯一预算并传递给下层。原有公开的无预算入口保持兼容,门面使用内部带预算入口。 + +**Tech Stack:** C# 10、.NET Standard 2.0、PowerShell 反射回归脚本、`Stopwatch`、`CancellationToken`。 + +## Global Constraints + +- 位置使用 m、地图输入使用 mm、航向使用 rad、曲率使用 1/m;不得改变现有单位边界。 +- 取消或超时必须返回空路径和空方向分段,绝不发布部分路径或部分 `PlanningGridMap`。 +- 地图不得依赖 CoarsePath;共享预算只能放在 `ParkrobTrajplanner/Utils`。 +- 每 256 个或更少循环工作单元检查一次预算;外部 `IMapObstacleSource.ProjectToWorld()` 是调用方提供的同步快照接口,只能在调用前后检查,不能强制抢占其内部执行。 +- 不改变碰撞保守性、运动原语、代价公式、目标候选保护或 Open List 排序。 +- 保留 `PlanningMapFactory.Create(request)`、`HybridAStarPlanner.Plan(request, token)`、`HybridAStarSearch.Search(request, token)` 和 `GridDijkstraHeuristic(map,row,col)` 的兼容入口。 +- 不执行 Git 添加、提交、重置或工作区清理。 + +--- + +## 文件结构 + +| 文件 | 职责 | +| --- | --- | +| `Utils/PlanningOperationBudget.cs` | 内部单调计时、取消检查和统一停止原因。 | +| `Map/PlanningMapBuildResult.cs` | 地图构建状态:成功、失败、取消、超时。 | +| `Map/Core/EnvironmentMapBuilder.cs`、`EnvironmentMapBuildResult.cs` | 将预算传入来源处理与栅格化,并保留停止原因。 | +| `Map/Obstacles/MapObstacleRasterizer.cs` | 在圆形/矩形逐格写入期间定期停止。 | +| `Map/Planning/{PlanningMapAdapter,ObstacleDistanceField,EuclideanDistanceTransform}.cs` | 在占据复制、EDT 和距离换算期间定期停止且不产出快照。 | +| `Map/PlanningMapFactory.cs` | 可取消地等待创建锁、检查缓存、建图、哈希和写缓存。 | +| `CoarsePath/Search/{GridDijkstraHeuristic,HybridAStarSearch}.cs` | 为 Dijkstra 和 Open List 使用共享预算,记录陈旧条目和峰值。 | +| `CoarsePath/{HybridAStarPlanner,Contracts/PlanningDiagnostics}.cs` | 将搜索统计传给最终结果,并让公开 Planner 包装兼容预算。 | +| `CoarsePath/Facade/CoarsePathPlanningService.cs` | 创建一次调用唯一预算,映射地图阶段停止状态。 | +| `Map/README.md`、`CoarsePath/README.md` | 分别说明地图构建状态,以及粗规划总超时和取消状态。 | +| `tests/verify_planning_map_factory.ps1`、`verify_planning_map_adapter.ps1`、`verify_coarse_path_search.ps1`、`verify_coarse_path_integration.ps1` | 预算与诊断的反射回归覆盖。 | + +### Task 1: 建立独立的操作预算与地图终止契约 + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` + +**Consumes:** `System.Diagnostics.Stopwatch`、`System.Threading.CancellationToken`。 + +**Produces:** `PlanningOperationStopReason`、`PlanningOperationBudget`、`PlanningMapBuildStatus`;下游 Map/CoarsePath 均只通过这些类型传递停止信息。 + +- [ ] **Step 1: 写失败测试,锁定新的地图状态与预算公开反射形状。** + + 在现存的 `verify_planning_map_factory.ps1` 断言存在内部 `MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget` 与三值 `PlanningOperationStopReason`,并断言 `PlanningMapBuildResult.Status` 存在;已取消、已超时结果的 `Succeeded` 必须为 `false`、`Map` 为 `$null`、`CacheHit` 为 `None`。 + + ```powershell + $statusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) + Assert-True ($statusType.GetEnumNames() -contains 'Cancelled') 'Map build status must expose cancellation.' + Assert-True ($statusType.GetEnumNames() -contains 'TimedOut') 'Map build status must expose timeout.' + Assert-True ($resultType.GetProperty('Status') -ne $null) 'Map result must expose an explicit status.' + ``` + +- [ ] **Step 2: 运行两个脚本,确认因类型或属性不存在而失败。** + + Run: + + ```powershell + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 + ``` + + Expected: 断言报告 `PlanningOperationBudget` 或 `PlanningMapBuildStatus` 缺失。 + +- [ ] **Step 3: 实现最小共享预算和显式地图状态。** + + `PlanningOperationBudget` 的核心接口固定如下;超时使用构造时启动的单调 `Stopwatch`,取消优先于超时: + + ```csharp + internal enum PlanningOperationStopReason { None, Cancelled, TimedOut } + + internal sealed class PlanningOperationBudget + { + internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout); + internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken); + internal TimeSpan Elapsed { get; } + internal PlanningOperationStopReason GetStopReason(); + internal PlanningOperationStopReason CheckEvery(ref int workItemCount); + } + ``` + + `CheckEvery` 在第一次工作单元以及每 256 个工作单元检查;`Unlimited` 不启用时间限制但仍响应取消。将地图结果从布尔构造改为状态构造: + + ```csharp + public enum PlanningMapBuildStatus { Success, Failed, Cancelled, TimedOut } + + public PlanningMapBuildStatus Status { get; } + public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } + + internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason reason, + IReadOnlyList sourceResults) + { + return new PlanningMapBuildResult( + reason == PlanningOperationStopReason.Cancelled + ? PlanningMapBuildStatus.Cancelled + : PlanningMapBuildStatus.TimedOut, + reason == PlanningOperationStopReason.Cancelled ? "地图创建已取消。" : "地图创建已超时。", + sourceResults, PlanningMapCacheHit.None, null); + } + ``` + +- [ ] **Step 4: 重跑两个脚本,确认新契约通过且旧地图缓存断言未回归。** + + Run: 与 Step 2 相同。 + + Expected: 地图工厂脚本输出 `Planning map factory checks passed.`。 + +### Task 2: 让地图创建、栅格化和距离场遵守预算 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs` +- Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` +- Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` + +**Consumes:** Task 1 的 `PlanningOperationBudget` 与 `PlanningOperationStopReason`。 + +**Produces:** `PlanningMapFactory` 的内部 `Create(PlanningMapRequest, PlanningOperationBudget)`;它在取消/超时时返回 `PlanningMapBuildResult.Stopped`,不会写入 LRU 缓存。 + +- [ ] **Step 1: 写失败测试,覆盖预先取消、EDT 中超时与缓存不污染。** + + 在工厂脚本创建一个已取消的 `CancellationTokenSource`,通过反射调用新的内部带预算 `Create`,断言: + + ```powershell + Assert-Equal 'Cancelled' $cancelledMapResult.Status.ToString() 'Cancelled map construction must report cancellation.' + Assert-False $cancelledMapResult.Succeeded 'Cancelled map construction must not succeed.' + Assert-Null $cancelledMapResult.Map 'Cancelled map construction must not publish a map.' + Assert-Equal 'None' $cancelledMapResult.CacheHit.ToString() 'Stopped construction must not publish a cache hit.' + ``` + + 在适配器脚本为含障碍的大栅格创建 `PlanningOperationBudget`,使用零超时调用内部 `TryCreate`,断言返回 `TimedOut` 且输出 `PlanningGridMap` 为 `$null`。随后用无预算入口再次创建相同地图,断言距离场仍可用,以证明停止时没有污染输入或缓存。 + +- [ ] **Step 2: 运行地图工厂和适配器脚本,确认新增反射入口缺失而失败。** + + Run: + + ```powershell + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 + ``` + + Expected: 新带预算 `Create` 或 `TryCreate` 反射查找失败。 + +- [ ] **Step 3: 在地图管线的全部长循环中传递并检查预算。** + + 实现以下内部接口,所有 `Try*` 在停止时返回 `false` 并把 `stopReason` 设为非 `None`;普通参数错误仍按现有失败原因或异常处理。 + + ```csharp + internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget); + internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, + PlanningOperationBudget budget, out PlanningOperationStopReason stopReason); + internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, + out PlanningGridMap map, out PlanningOperationStopReason stopReason); + internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, + PlanningOperationBudget budget, out ObstacleDistanceField field, + out PlanningOperationStopReason stopReason); + internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, + PlanningOperationBudget budget, out double[] squared, + out PlanningOperationStopReason stopReason); + internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget); + ``` + + 保留现有公开 `Build`、`Rasterize`、`PlanningMapAdapter.Create`、`ObstacleDistanceField.Create` 与 `ComputeSquaredDistances`,让它们以 `PlanningOperationBudget.Unlimited(CancellationToken.None)` 包装新入口。对圆形/矩形逐格循环、EDT 两遍扫描、`Transform1D` 两个 `q` 循环、距离场扫描均调用 `budget.CheckEvery(ref workItemCount)`。 + + 工厂以 `Monitor.TryEnter(_createGate, 16)` 循环等待创建锁;每次失败后检查预算。拿到锁后立刻再检查预算,随后在缓存读、来源处理、适配、占据哈希和每次缓存写入前检查。将 SHA-256 改为每 4096 字节调用 `TransformBlock` 的增量哈希,并在块间检查预算。任一非 `None` 停止原因直接返回 `PlanningMapBuildResult.Stopped`,且不会执行 `_cache.AddOccupancy` 或 `_cache.AddInput`。 + +- [ ] **Step 4: 重跑地图脚本,确认普通建图、两级缓存和新停止结果同时通过。** + + Run: 与 Step 2 相同。 + + Expected: `Planning map factory checks passed.` 与 `Planning map adapter checks passed.`。 + +### Task 3: 为 Dijkstra 与 Hybrid A* 使用同一预算并记录 Open List 统计 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Task 1 的预算和 Task 2 不可变地图;现有 `BinaryMinHeap`。 + +**Produces:** `GridDijkstraHeuristic.TryCreate`、带共享预算的内部搜索/规划入口,以及 `HybridAStarSearchResult.StaleOpenListEntryCount` 和 `PeakOpenListCount`。 + +- [ ] **Step 1: 写失败测试,覆盖 Dijkstra 中取消、总超时和统计透传。** + + 在搜索脚本上创建至少 500×500 格的已就绪空地图,在地图创建完成后启动 `CancellationTokenSource.CancelAfter(1)` 并调用搜索。断言返回 `Cancelled`、`SuccessNodeIndex` 为 `$null`、运行时间小于 2 秒。再用同一地图和 `SearchTimeout = TimeSpan.Zero` 断言 `SearchTimeout`,以证明启发式创建前即尊重总预算。 + + 反射断言搜索结果的新属性存在,并让直接路径搜索至少满足: + + ```powershell + Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entries.' + Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak size.' + Assert-True ($searchResult.PeakOpenListCount -ge 1) 'A successful search must retain at least one Open List entry.' + Assert-True ($searchResult.StaleOpenListEntryCount -ge 0) 'Stale Open List entry count must never be negative.' + ``` + + 在集成脚本断言 `PlanningResult.Diagnostics` 中的两项值与搜索结果一致;对产生重开/失效条目的固定障碍场景断言陈旧条目数大于零。 + +- [ ] **Step 2: 运行搜索和集成脚本,确认新增属性、带预算入口或中途取消断言失败。** + + Run: + + ```powershell + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + ``` + + Expected: 新属性或 Dijkstra 中止行为缺失导致失败;现有“搜索前取消”检查不应被视为通过中途取消测试。 + +- [ ] **Step 3: 实现预算感知的 Dijkstra、搜索与真实诊断。** + + `GridDijkstraHeuristic` 保留当前公开构造函数,并增加内部可失败构建: + + ```csharp + internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, + PlanningOperationBudget budget, out GridDijkstraHeuristic heuristic, + out PlanningOperationStopReason stopReason); + ``` + + `Build` 的每次出堆与每 256 个邻居检查预算;停止时不返回部分启发式。`HybridAStarSearch.Search(request, token)` 从 `request.Configuration.SearchTimeout` 创建预算作为兼容包装,新增内部: + + ```csharp + internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget); + ``` + + 删除搜索器内部新建的 `Stopwatch` 与 `IsTimedOut`,所有原有取消/超时位置改为读取 `budget.GetStopReason()` 并精确映射为 `PlanningStatus.Cancelled` 或 `PlanningStatus.SearchTimeout`。Dijkstra 返回停止原因时立即返回对应搜索状态;节点上限仍只在预算检查之后、真正扩展之前检查。 + + 扩展搜索结果构造函数与只读属性: + + ```csharp + public int StaleOpenListEntryCount { get; } + public int PeakOpenListCount { get; } + ``` + + 每次 `openList.Push` 后执行 `peakOpenListCount = Math.Max(peakOpenListCount, openList.Count)`。普通节点出堆后因 best-G 已更新、节点索引不匹配或已关闭而跳过时递增 `staleOpenListEntryCount`;目标候选的出队复核失败不算陈旧条目。所有 `CreateResult` 调用传递两项计数。 + + `HybridAStarPlanner` 的公开 `Plan` 保持签名并建立自己的预算;新增内部 `Plan(request, budget)` 供门面调用。诊断使用 `budget.Elapsed`,并把两个搜索统计填入原本为零的构造参数: + + ```csharp + searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, + searchResult == null ? 0 : searchResult.PeakOpenListCount, + ``` + +- [ ] **Step 4: 重跑搜索和集成脚本,确认取消、超时、目标候选与统计均通过。** + + Run: 与 Step 2 相同。 + + Expected: `Coarse path search primitive checks passed.`、`Coarse path Hybrid A star search checks passed.`、`Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 + +### Task 4: 让业务门面映射地图阶段终止状态并更新调用文档 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/README.md` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Task 1 的地图状态、Task 2 的带预算工厂入口、Task 3 的内部 Planner 入口。 + +**Produces:** 一次 `CoarsePathPlanningService.Plan` 的统一预算和对调用方稳定的 `PlanningStatus` 映射。 + +- [ ] **Step 1: 写失败测试,锁定门面状态映射和空结果。** + + 在集成脚本使用已取消 Token 调用门面,断言: + + ```powershell + Assert-Equal 'Cancelled' $facadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' + Assert-Equal 'Cancelled' $facadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' + Assert-Equal 0 $facadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' + Assert-Equal 0 $facadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' + ``` + + 对 `SearchTimeout = TimeSpan.Zero` 的有效 job,断言地图结果和规划结果分别为 `TimedOut`、`SearchTimeout`,且调试 sink 不会把已停止操作改写为成功。 + +- [ ] **Step 2: 运行集成脚本,确认当前门面把地图阶段停止误报为 `InvalidMap` 或继续建图。** + + Run: + + ```powershell + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + ``` + + Expected: 地图状态属性或正确的 `Cancelled`/`SearchTimeout` 映射不存在。 + +- [ ] **Step 3: 让门面创建并传递唯一预算,随后更新 README。** + + 门面从有效 `job.Configuration.SearchTimeout` 创建 `PlanningOperationBudget`;配置为空或时间值非法时使用无超时预算,让既有 Planner 预检继续返回原有无效配置状态。依次调用: + + ```csharp + PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); + if (!mapResult.Succeeded) + { + PlanningStatus status = mapResult.Status == PlanningMapBuildStatus.Cancelled + ? PlanningStatus.Cancelled + : mapResult.Status == PlanningMapBuildStatus.TimedOut + ? PlanningStatus.SearchTimeout + : PlanningStatus.InvalidMap; + return PublishDebug(job, mapResult, PlanningResult.Failure(status, diagnostics)); + } + PlanningResult planningResult = _planner.Plan(request, budget); + ``` + + `Map/README.md` 在 `PlanningMapBuildResult` 的说明处增加 `Status`:`Success`、`Failed`、`Cancelled`、`TimedOut`;后两种不提供地图也不会进入缓存。`CoarsePath/README.md` 增加“总预算与取消”小节:`SearchTimeout` 是从门面开始的总预算,覆盖建图、距离场、Dijkstra 与 Hybrid A*;`Cancelled`/`SearchTimeout` 一律无路径;不应以 `InvalidMap` 重试用户主动取消。 + +- [ ] **Step 4: 重跑集成脚本,确认门面状态映射、缓存复用和 debug 旁路隔离均通过。** + + Run: 与 Step 2 相同。 + + Expected: `Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 + +### Task 5: 全量回归与验收记录 + +**Files:** + +- Modify only if a command reveals a concrete regression: the exact responsible source or test file from Tasks 1–4. + +- [ ] **Step 1: 执行 Debug 构建。** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + ``` + + Expected: `0 个警告`、`0 个错误`。 + +- [ ] **Step 2: 执行全部现行 P0 地图与粗规划回归。** + + Run: + + ```powershell + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 + powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + ``` + + Expected: 每个脚本退出码为 0 并输出 `passed`。 + +- [ ] **Step 3: 对照设计完成验收。** + + 逐项检查:预先取消和中途 Dijkstra 取消均返回 `Cancelled`;零总超时返回 `SearchTimeout`;地图停止不创建快照或缓存条目;正常输入的路径与缓存行为不变;诊断两项不再硬编码为零;README 已说明总预算语义。 + +## 自检 + +- 规格覆盖:Task 1 定义共享预算和显式地图状态;Task 2 覆盖地图、EDT、锁和缓存;Task 3 覆盖 Dijkstra、Hybrid A*、统计和 Planner;Task 4 覆盖门面映射与文档;Task 5 覆盖完整回归。 +- 类型一致性:所有耗时组件仅接收 `PlanningOperationBudget` 并输出 `PlanningOperationStopReason`;Map 使用 `PlanningMapBuildStatus`,粗规划使用既有 `PlanningStatus`。 +- 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。 diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md b/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md new file mode 100644 index 0000000..92e539d --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-coarse-path-live-amr-scenarios.md @@ -0,0 +1,436 @@ +# 固定粗路径案例使用实时 AMR 位姿 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让全部 `粗路径规划-*` 固定 MovementTest 从一次冻结的当前 AMR 世界坐标和航向开始规划,并把案例地图、目标和障碍平移到 AMR 附近。 + +**Architecture:** `CoarsePathScenarioFactory` 保留确定性的基准案例入口,并新增接受 AMR mm/deg 位姿的入口。一个仅属于固定场景工厂的平移对象将基准案例的起点映射为 AMR 起点,平移地图和障碍,并以起终点基准航向差计算新的终点航向。MovementTest 运行器在前台读取一次 `DetourInterface.getCartLocation()`,校验并冻结快照后才提交已有后台规划流程。 + +**Tech Stack:** C# / .NET Standard 2.0、Clumsy `MovementTest` 与 Painter、现有 `CoarsePathPlanningService`、PowerShell 反射验证脚本。 + +## Global Constraints + +- 保留 `CoarsePathScenarioFactory.Create(CoarsePathTestScenario)` 的现有确定性行为,自动化离线测试继续使用它。 +- 新实时入口的 X/Y 单位为世界 mm、航向单位为 deg;核心 `Pose2D` 仍为 m/rad。 +- 固定场景仅做位置平移;TwoLeg 的 `DetectionHeadingRadians` 不得因 AMR 航向改变。 +- 起点航向必须等于冻结 AMR 航向;终点航向必须保持基准案例的起终点航向差,并规范化到 `[-pi, pi]`。 +- 不改变 Hybrid A*、车辆参数、碰撞模型、地图缓存实现、手动 `粗路径规划` 的终点/障碍/超时输入流程,或任何底盘控制行为。 +- 实时位姿为空、读取抛异常或 X/Y/航向不是有限数时,不得创建后台任务;必须向用户报告以 `AMR 位姿不可用` 开头的原因。 +- 不暂存工作区中的无关改动;每次提交均明确列出文件路径。 + +## File Structure + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` + - 固定场景的基准坐标定义、实时 AMR 工厂重载、平移与航向转换的唯一实现位置。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` + - 固定场景的 AMR 快照读取、输入失败提示,以及冻结位姿的 Painter 状态显示。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + - 对新工厂重载、平移几何、航向规则、缓存和基准入口不回归的程序集级验证。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` + - 对运行器只读一次实时位姿、传入实时工厂入口及 AMR 失败/状态文案的源级验证。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` + - 说明六个固定案例的实时锚定规则、缓存命中条件和定位异常行为。 + +--- + +### Task 1: 为固定案例建立可验证的实时 AMR 平移工厂 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:303-399` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs:115-284` + +**Interfaces:** + +- Consumes: `CoarsePathTestScenario`、`PlanningMapRequest`、`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource` 和 `TwoLegProjectionInput`。 +- Produces: `public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。 +- Produces: 每个固定案例以 `FixedScenarioTransform` 统一转换的起点、终点、地图边界和来源快照;旧 `Create(scenario)` 仍创建原始基准几何。 + +- [ ] **Step 1: 在程序集级验证脚本中写出失败测试** + + 在 `$factoryCreate` 定义之后加入实时重载查找;在基准案例循环之后加入以下断言。测试锚点 `(12000, -3000, 90)` 对单矩形场景的偏移应为 `(+11000, -5000)` mm;多来源锚点 `(7000, 8000, 45)` 对该基准场景的偏移应为 `(+6000, +7000)` mm。 + + ```powershell + $factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @( + $scenarioEnumType, [double], [double], [double]) + Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).' + + foreach ($scenarioName in $scenarioNames) { + $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) + $liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0)) + Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X." + Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y." + Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading." + } + + $rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour') + $liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0)) + Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.' + Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.' + Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.' + Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.' + Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.' + Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.' + Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.' + Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.' + $rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld() + $rectangleObstacle = $rectangleProjection.Obstacles[0] + Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.' + Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.' + + $multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg') + $baselineMulti = $factoryCreate.Invoke($null, @($multiScenario)) + $liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0)) + Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.' + Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.' + Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.' + $baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles + $liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles + Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.' + Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.' + + $noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath') + $liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0)) + Assert-Near 9000.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.' + Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.' + Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.' + + $invalidLivePoseRejected = $false + try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) } + catch [Reflection.TargetInvocationException] { + $invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException] + } + Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.' + ``` + +- [ ] **Step 2: 运行该测试,确认它因缺少实时工厂重载而失败** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + ``` + + Expected: build succeeds; the script fails at `Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).` + +- [ ] **Step 3: 以单一平移对象实现实时工厂重载** + + 在 `CoarsePathScenarioFactory` 中保留一参 `Create`,并用一个私有锚点和变换对象驱动同一个 switch。使用以下接口形状;`CreateCore` 的每个 case 都先以该案例的基准起点创建 `FixedScenarioTransform`,再传给对应的场景构造函数。 + + ```csharp + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) + { + return CreateCore(scenario, null); + } + + public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, + double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees) + { + return CreateCore(scenario, + new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees)); + } + + private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, + FixedScenarioAnchor anchor) + { + switch (scenario) + { + case CoarsePathTestScenario.ExplicitEmpty: + return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.RectangleDetour: + case CoarsePathTestScenario.CacheHit: + return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.ManualAndTwoLeg: + return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor)); + case CoarsePathTestScenario.ReverseGearSwitch: + return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + case CoarsePathTestScenario.NoFeasiblePath: + return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor)); + default: + throw new ArgumentOutOfRangeException(nameof(scenario)); + } + } + ``` + + Implement the two private classes in the same file. They must validate all public live inputs with existing `EnsureFinite`, keep the identity transform when `anchor` is null, convert fixed mm to metres only when creating `Pose2D`, and normalize headings without changing a position. + + ```csharp + private sealed class FixedScenarioAnchor + { + public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees) + { + EnsureFinite(xMillimeters, nameof(xMillimeters)); + EnsureFinite(yMillimeters, nameof(yMillimeters)); + EnsureFinite(headingDegrees, nameof(headingDegrees)); + XMillimeters = xMillimeters; + YMillimeters = yMillimeters; + HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians); + } + + public double XMillimeters { get; } + public double YMillimeters { get; } + public double HeadingRadians { get; } + } + + private sealed class FixedScenarioTransform + { + private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians) + { + DeltaXMillimeters = deltaXMillimeters; + DeltaYMillimeters = deltaYMillimeters; + HeadingDeltaRadians = headingDeltaRadians; + } + + public double DeltaXMillimeters { get; } + public double DeltaYMillimeters { get; } + public double HeadingDeltaRadians { get; } + + public static FixedScenarioTransform From(double baselineStartXMillimeters, + double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor) + { + if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d); + return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters, + anchor.YMillimeters - baselineStartYMillimeters, + NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians)); + } + + public float X(float value) { return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); } + public float Y(float value) { return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); } + public Pose2D Pose(double xMeters, double yMeters, double headingRadians) + { + return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter, + (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter, + NormalizeRadians(headingRadians + HeadingDeltaRadians)); + } + } + + private static double NormalizeRadians(double angle) + { + double normalized = angle % (2d * Math.PI); + if (normalized <= -Math.PI) return normalized + 2d * Math.PI; + return normalized > Math.PI ? normalized - 2d * Math.PI : normalized; + } + ``` + + Update every fixed builder to accept `FixedScenarioTransform transform` and use `transform.Pose`, `transform.X` and `transform.Y` for all coordinates: + + ```csharp + new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f), + transform.Y(1200f), transform.Y(2800f)); + new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f); + new TwoLegProjectionInput(true, transform.X(3900f), transform.Y(2500f), 0d, + -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot."); + ``` + + Change `CreateMap` to accept the transform and translate all four `MapBoundsMm` limits with the correct axis. It must not change resolution, source IDs, source versions, required flags, empty-map flags, vehicle parameters, direction constraints or existing timeout assignments. + +- [ ] **Step 4: 运行工厂和回归验证,确认新旧入口都通过** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + ``` + + Expected: build has zero errors; script ends with `Coarse path P1 scenario checks passed.` Existing assertions for the one-argument factory must continue to pass. + +- [ ] **Step 5: 提交仅包含工厂与程序集级测试的可审查变更** + + ```powershell + git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1 + git commit -m "feat: anchor coarse path scenarios to AMR pose" + ``` + +### Task 2: 让固定 MovementTest 冻结 AMR 快照并显示诊断 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-52` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:221-371,512-540` + +**Interfaces:** + +- Consumes: 新的 `CoarsePathScenarioFactory.Create(scenario, xMm, yMm, headingDeg)`、`DetourInterface.getCartLocation()` 和现有 `Run(string, CoarsePathPlanningJob)`。 +- Produces: `RunScenario` 只读取一次位姿,创建已冻结的实时 job;手动入口继续调用现有两参 `Run`。 +- Produces: 私有 `AmrPoseSnapshot`,仅保存有限 X/Y/deg 和格式化后的状态文本,不向规划后台读取定位。 + +- [ ] **Step 1: 在 UI 源级验证中加入失败断言** + + 在现有 `$source` 断言后追加以下代码。使用 ASCII 的方法名和 `AMR` 文本,避免 Windows PowerShell 5 对中文脚本字符串的编码歧义。 + + ```powershell + $runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner') + Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.' + $runnerSource = $source.Substring($runnerStart) + Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.' + Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.' + Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.' + Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.' + Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.' + Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.' + ``` + +- [ ] **Step 2: 运行 UI 脚本,确认新增实时位姿约束尚未满足** + + Run: + + ```powershell + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 + ``` + + Expected: script fails at `Fixed scenarios must read the current AMR pose.` or `Fixed scenarios must use the AMR-aware factory overload.` + +- [ ] **Step 3: 实现单次 AMR 读取、校验、冻结与绘制传递** + + 在 `CoarsePathPlanningTestRunner` 内加入以下私有快照类型和工厂调用路径。任何读取或校验异常都在前台转换为 `AMR 位姿不可用:...`,因此不会进入 `Task.Run`。 + + ```csharp + 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"; + } + } + } + + private static void EnsureFiniteAmrValue(double value, string name) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + throw new ArgumentException(name + " 必须是有限数。"); + } + + internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName) + { + try + { + var pose = DetourInterface.getCartLocation(); + 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)); + } + } + ``` + + Keep the existing `internal static void Run(string scenarioName, CoarsePathPlanningJob job)` as a thin compatibility overload that calls a new private `Run(string, CoarsePathPlanningJob, AmrPoseSnapshot)`. Thread the snapshot through `DrawPending`, the `ContinueWith` lambda, `Finish`, `DrawResult`, and `DrawStatus`. In `DrawStatus`, render `snapshot.DisplayText` after the scene line only when the snapshot is non-null; increase subsequent Y offsets consistently so vehicle and termination-reason text do not overlap. Manual `CoarsePathPlanningTest.Test()` must keep its present `Run(..., job)` call and user-entered timeout behavior. + +- [ ] **Step 4: 编译并运行 UI 验证,确认后台边界未回归** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 + ``` + + Expected: build has zero errors; script ends with `Coarse path UI source checks passed.` Existing assertions still confirm one shared service, background `Task.Run`, cancellation and no blocking `Task.Result`/`Wait`. + +- [ ] **Step 5: 提交仅包含运行器与 UI 验证的可审查变更** + + ```powershell + git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/tests/verify_coarse_path_ui.ps1 + git commit -m "feat: use live AMR pose in coarse path tests" + ``` + +### Task 3: 记录实时固定场景语义并执行完整回归 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:289-347` +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:54-100` + +**Interfaces:** + +- Consumes: 已实现的实时工厂入口和 MovementTest 状态文本。 +- Produces: 可供现场使用者理解的固定案例锚定、缓存和定位异常说明;文档验证继续只检查稳定 ASCII 标识符。 + +- [ ] **Step 1: 在 README 源级验证中加入失败断言** + + 在 `$readmeStructure` 数组中加入以下 ASCII 项,并在数组后加入 `Contains` 断言: + + ```powershell + foreach ($requiredLiveAmrText in @( + 'Create(CoarsePathTestScenario scenario, double amrXMillimeters', + 'AMR', + 'Input', + 'TwoLeg')) { + Assert-True ($readme.Contains($requiredLiveAmrText)) "P1 README must document live fixed scenarios: $requiredLiveAmrText." + } + ``` + +- [ ] **Step 2: 运行 UI 文档验证,确认文档尚未描述实时固定案例** + + Run: + + ```powershell + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 + ``` + + Expected: script fails because README does not yet contain the new `Create(CoarsePathTestScenario scenario, double amrXMillimeters` signature. + +- [ ] **Step 3: 更新 P1 手动测试与可视化章节** + + 在固定案例表之前新增“固定案例的实时 AMR 锚点”小节,逐条说明:六个 `粗路径规划-*` 入口读取一次 `getCartLocation`;起点等于这份冻结的世界 mm/deg 位姿;地图边界、目标、圆形/轴对齐矩形和 TwoLeg 检测原点统一平移;终点航向保持和基准起点的航向差;TwoLeg 朝向不旋转;手动 `粗路径规划` 的输入流程不变。 + + 在同一小节以单行 inline code 明确列出公开签名:`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。 + + 加入公开签名示例: + + ```csharp + CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create( + CoarsePathTestScenario.RectangleDetour, + amrXMillimeters, amrYMillimeters, amrHeadingDegrees); + ``` + + 将缓存命中表项改为“相同 AMR X/Y 下的重复完整地图输入”;明确 AMR 位置已移动时未命中是正常的 `None`/非 `Input` 缓存结果,且仅航向变化不改变地图输入、仍可命中。补充运行器在定位读取、空值访问或有限数校验失败时显示 `AMR 位姿不可用` 且不启动后台规划;状态区会显示该次冻结的 AMR 起点。保留关于 `getCartLocation` 可能阻塞和必须在定位准备完成后运行的既有警告。 + +- [ ] **Step 4: 运行完整粗路径与地图回归集** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 + ``` + + Expected: build reports zero warnings and zero errors. Every verification script exits 0; integration ends with `Coarse path P1 scenario checks passed.` and UI ends with `Coarse path UI source checks passed.` + +- [ ] **Step 5: 提交文档与最后验证脚本变更** + + ```powershell + git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1 + git commit -m "docs: explain live AMR coarse path scenarios" + ``` diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md b/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md new file mode 100644 index 0000000..929f053 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-coarse-path-search-elapsed.md @@ -0,0 +1,256 @@ +# Coarse Path Search Elapsed Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在保留总耗时和总超时语义的前提下,记录并显示地图就绪后生成最终粗路径的独立耗时。 + +**Architecture:** `PlanningDiagnostics` 增加兼容的 `PathSearchElapsed` 只读字段。`HybridAStarPlanner` 在调用 `HybridAStarSearch.Search` 前启动本地秒表,并把搜索、回溯、装配和最终复核的耗时传入诊断对象;门面总预算和 `Elapsed` 不改变。MovementTest 图层和 Toast 同时显示总耗时与路径搜索耗时。 + +**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 回归脚本、Clumsy `MovementTest` Painter。 + +## Global Constraints + +- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 入口开始的总耗时。 +- `PathSearchElapsed` 不包括地图来源读取、缓存、栅格化和距离场构建。 +- `PathSearchElapsed` 包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核。 +- 搜索开始前失败时 `PathSearchElapsed` 必须为 `TimeSpan.Zero`;搜索阶段失败时保留已消耗时间。 +- 新构造函数参数必须放在现有参数之后并提供默认值,保持现有位置参数调用的兼容性。 +- 不修改 `PlanningOperationBudget`、取消机制、超时预算或地图缓存行为。 +- 不执行 Git 操作。 + +## 文件结构 + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开独立路径搜索耗时。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 在地图就绪后的路径产出阶段计时,并写入诊断对象。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 在状态图层和 Toast 显示两种耗时。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 说明两个耗时的计时边界。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证真实路径搜索耗时和地图阶段失败的零值。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证 UI/README 使用新字段。 + +--- + +### Task 1: 路径搜索耗时诊断契约 + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:100-108,199-204` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:10-60` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-105,185-210` + +**Interfaces:** +- Consumes: `PlanningResult.Diagnostics.Elapsed`、`HybridAStarPlanner.Plan(PlanningRequest, PlanningOperationBudget)`。 +- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`。 +- Contract: 成功结果满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`;地图阶段取消的结果为 `TimeSpan.Zero`。 + +- [ ] **Step 1: 在集成脚本写入失败断言** + +在空地图成功规划的现有诊断断言之后加入: + +```powershell +Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` + 'Planner diagnostics must retain a non-negative path-search elapsed time.' +Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) ` + 'Path-search elapsed time must not exceed total planning elapsed time.' +``` + +在 `$cancelledFacadeResult` 的现有断言之后加入: + +```powershell +Assert-Equal ([TimeSpan]::Zero) $cancelledFacadeResult.PlanningResult.Diagnostics.PathSearchElapsed ` + 'Map-stage cancellation must not report path-search time.' +``` + +- [ ] **Step 2: 运行集成脚本并确认红灯** + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` + +Expected: FAIL,提示 `PathSearchElapsed` 不存在或无法通过新增的路径搜索耗时断言;此前的地图和门面检查仍先通过。 + +- [ ] **Step 3: 以兼容形式扩展诊断对象** + +在 `PlanningDiagnostics` 构造函数的最后一个参数之后增加: + +```csharp +TimeSpan pathSearchElapsed = default(TimeSpan) +``` + +并在构造函数中加入: + +```csharp +PathSearchElapsed = pathSearchElapsed; +``` + +在 `Elapsed` 属性之后加入: + +```csharp +/// +/// 地图就绪后搜索、回溯、装配和最终复核得到最终粗路径的耗时;不含建图。搜索开始前失败时为零。 +/// +public TimeSpan PathSearchElapsed { get; } +``` + +同时更新构造函数 XML 注释,明确 `elapsed` 是总耗时而 `pathSearchElapsed` 是不含建图的路径产出耗时。 + +- [ ] **Step 4: 在规划器的正确边界计时** + +在 `HybridAStarPlanner.cs` 顶部加入: + +```csharp +using System.Diagnostics; +``` + +在 `Plan(PlanningRequest request, PlanningOperationBudget budget)` 的 `try` 外部声明: + +```csharp +Stopwatch pathSearchStopwatch = null; +``` + +在第二次 `budget.GetStopReason()` 通过、且紧接 `_search.Search(request, budget)` 前写入: + +```csharp +pathSearchStopwatch = Stopwatch.StartNew(); +HybridAStarSearchResult searchResult = _search.Search(request, budget); +``` + +将 `Failure` 签名扩展为: + +```csharp +private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, + HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) +``` + +并在内部取得: + +```csharp +TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; +return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, reason, + pathSearchElapsed)); +``` + +将 `_search.Search` 之后的每个失败返回和 `catch` 都传入 `pathSearchStopwatch`。成功结果调用改为: + +```csharp +return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, + minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); +``` + +最后将 `CreateDiagnostics` 扩展为接收最后一个 `TimeSpan pathSearchElapsed` 参数,并将其作为 `PlanningDiagnostics` 的最后一个实参传入。 + +- [ ] **Step 5: 运行集成脚本并确认绿灯** + +Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` + +Expected: exit code 0;只允许项目既有的过时 API 警告。 + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` + +Expected: `Coarse path integration checks passed.`、`Coarse path facade checks passed.`、`Coarse path P1 scenario checks passed.`。 + +### Task 2: 图层、Toast 与 README 展示 + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-70` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:494-513` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:285-337` + +**Interfaces:** +- Consumes: `result.PlanningResult.Diagnostics.Elapsed` 和 Task 1 提供的 `PathSearchElapsed`。 +- Produces: 状态图层与 Toast 中的“总耗时”和“路径搜索”文本,以及对应 README 说明。 +- Contract: 展示值均使用毫秒、`InvariantCulture` 和 `F0` 格式;不改变后台任务、取消或 Painter 图层名称。 + +- [ ] **Step 1: 为 UI 和 README 写入失败检查** + +在 UI 源码断言区域加入: + +```powershell +Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) ` + 'The status layer and Toast must both show path-search elapsed time.' +Assert-Match $source '路径搜索' 'The UI must label the independent path-search elapsed time.' +``` + +在 `$readmeStructure` 数组加入: + +```powershell +'PathSearchElapsed', +'路径搜索耗时', +``` + +- [ ] **Step 2: 运行 UI 脚本并确认红灯** + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` + +Expected: FAIL,提示状态图层与 Toast 尚未显示 `PathSearchElapsed`,或 README 尚未说明该字段。 + +- [ ] **Step 3: 同时更新状态图层和 Toast** + +在 `DrawStatus` 中将单一耗时文本替换为: + +```csharp +Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + result.PlanningResult.Status + ",总耗时:" + + result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + + " ms,路径搜索:" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + + " ms", x, y + 240f); +``` + +在 `BuildToastMessage` 中将返回字符串替换为: + +```csharp +return "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + ",规划=" + + result.PlanningResult.Status + ",总耗时=" + + result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + + "ms,路径搜索=" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + + "ms。"; +``` + +- [ ] **Step 4: 补充 README 的耗时定义** + +在 “P1 手动测试与可视化” 的 “后台执行、停止与图层” 小节,在当前 `PlanningGridMap` 说明之后插入: + +```markdown +状态图层和 Toast 同时显示总耗时与 `PathSearchElapsed`(路径搜索耗时)。总耗时从 +`CoarsePathPlanningService.Plan` 入口开始,包含地图创建;路径搜索耗时从地图和起终点 +预检通过、即将进入 Hybrid A* 时开始,包含二维启发式、Hybrid A*、回溯、装配与最终复核, +不包含建图。搜索开始前即失败时该值为 0 ms。 +``` + +- [ ] **Step 5: 运行 UI 检查并确认绿灯** + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` + +Expected: `Coarse path P1 UI source checks passed.`。 + +### Task 3: 最终回归与验收 + +**Files:** +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` + +**Interfaces:** +- Consumes: 任务 1 和任务 2 的已完成代码与脚本。 +- Produces: 已验证的构建、集成回归和 UI/README 检查结果。 + +- [ ] **Step 1: 重新阅读计时边界** + +确认 `PathSearchElapsed` 只在 `_search.Search` 前启动;所有搜索后成功和失败路径均使用同一秒表;任何搜索前返回保持零值;总预算仍由 `PlanningOperationBudget` 控制。 + +- [ ] **Step 2: 运行最终构建** + +Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore` + +Expected: exit code 0。 + +- [ ] **Step 3: 运行最终自动化验证** + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1` + +Expected: `Coarse path P1 UI source checks passed.`。 + +Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1` + +Expected: `Coarse path integration checks passed.`、`Coarse path facade checks passed.`、`Coarse path P1 scenario checks passed.`。 + +- [ ] **Step 4: 进行手动界面验收** + +在可用 Clumsy 界面运行“粗路径规划-显式空图”或“粗路径规划-单矩形绕行”,确认状态图层和 Toast 都包含 `总耗时` 与 `路径搜索` 两个毫秒值,且点击停止仍能取消当前任务。 + diff --git a/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md b/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md new file mode 100644 index 0000000..abe7ab7 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-coarse-path-test-diagnostics.md @@ -0,0 +1,837 @@ +# Coarse Path Manual Test Diagnostics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让 `[MovementTest(name = "粗路径规划")]` 支持每次输入正数秒总预算,并在失败时显示真实终止原因、搜索统计、总耗时和路径搜索耗时。 + +**Architecture:** `HybridAStarSearchResult` 在搜索边界保留原始终止原因,`HybridAStarPlanner` 负责把原因、资源限制和节点统计装配成公开诊断。`PlanningDiagnostics` 增加兼容的 `PathSearchElapsed`,MovementTest 只覆盖本次任务的超时配置并在图层和 Toast 中显示诊断;搜索、安全和路径发布规则保持不变。 + +**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 反射回归脚本、Clumsy `MovementTest`/Painter。 + +## Global Constraints + +- 普通规划失败继续通过 `PlanningResult` 返回,不把超时、无解、碰撞或资源上限改成异常。 +- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 开始、包含建图的总耗时。 +- `PlanningDiagnostics.PathSearchElapsed` 从搜索前预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配和最终复核,不包含建图。 +- 搜索前失败的 `PathSearchElapsed` 为 `TimeSpan.Zero`;搜索后的所有出口满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`。 +- 手动超时只接受 `TimeSpan` 可表示范围内的有限正数秒;`0` 不表示不限时。 +- 不修改碰撞步长、终点容差、Open List 排序、地图边界策略、倒车开关或成功路径发布条件。 +- 不实现 Reeds-Shepp/Dubins 精确连接、横移、蟹行、原地旋转、平滑、速度规划或控制。 +- 手动入口继续使用长 `0.80 m`、宽 `0.60 m`、安全余量 `0.05 m`、最小转弯半径 `1.20 m` 的固定演示车辆参数。 +- 工作区已有未提交内容;每次提交只能暂存任务中明确列出的文件,不得暂存其他路径。 + +## File Structure + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` — 搜索结果保留发生位置一致的原始终止原因。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开地图就绪后的独立路径搜索耗时。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 计时搜索/发布阶段并组合原因、资源上限和节点统计。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 读取单次超时并在图层/Toast 显示诊断。 +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 记录手动预算、演示车辆、两类耗时和第一版能力边界。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` — 验证搜索原始原因和内部异常不再静默。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证规划器诊断、路径搜索耗时和慢可行场景。 +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证手动超时、图层/Toast 和 README 文本。 + +--- + +### Task 1: Preserve Search Termination Reasons + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1:302-391` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs:15-318` + +**Interfaces:** +- Consumes: `HybridAStarSearch.Search(PlanningRequest, CancellationToken)` 和内部 `Search(PlanningRequest, PlanningOperationBudget)`。 +- Produces: `HybridAStarSearchResult.TerminationReason : string`,成功时为空,所有失败状态非空。 +- Produces: `CreateResult(..., int? successNodeIndex, string terminationReason = null)`,未显式提供原因时按状态生成稳定中文原因。 + +- [ ] **Step 1: Add failing search-result reason assertions** + +在搜索结果属性断言后加入: + +```powershell +Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) ` + 'Search result must expose its original termination reason.' +``` + +在取消、节点上限和超时状态断言后分别加入: + +```powershell +Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) ` + 'Cancelled search must retain a non-empty reason.' +Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) ` + 'Node-limited search must retain a non-empty reason.' +Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) ` + 'Timed-out search must retain a non-empty reason.' +Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) ` + 'Cancelled and node-limited searches must retain different reasons.' +Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) ` + 'Node-limited and timed-out searches must retain different reasons.' +``` + +在脚本末尾通过内部预算重载制造一个可重复的未预期错误,并断言异常类型没有被吞掉: + +```powershell +$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') | + Where-Object { + $_.Name -eq 'Search' -and + $_.GetParameters().Length -eq 2 -and + $_.GetParameters()[1].ParameterType -eq $operationBudgetType + } | + Select-Object -First 1 +Assert-True ($internalSearchMethod -ne $null) ` + 'Search must retain its internal shared-budget overload.' +$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null)) +Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() ` + 'A missing internal budget must be mapped to InternalError.' +Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) ` + 'Internal search errors must retain the exception type.' +``` + +- [ ] **Step 2: Run the search script and verify RED** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +``` + +Expected: build succeeds; the script fails first with `Search result must expose its original termination reason.` + +- [ ] **Step 3: Extend `HybridAStarSearchResult`** + +Add the final constructor parameter, assignment, and public property: + +```csharp +internal HybridAStarSearchResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason) +{ + Status = status; + Nodes = new ReadOnlyCollection( + new List(nodes ?? Array.Empty())); + ExpandedNodeCount = expandedNodeCount; + GeneratedNodeCount = generatedNodeCount; + ReopenedNodeCount = reopenedNodeCount; + StaleOpenListEntryCount = staleOpenListEntryCount; + PeakOpenListCount = peakOpenListCount; + SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null; + TerminationReason = status == PlanningStatus.Success + ? string.Empty + : terminationReason ?? string.Empty; +} + +/// 搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。 +public string TerminationReason { get; } +``` + +- [ ] **Step 4: Centralize default reasons and retain exception details** + +Replace `CreateResult` with: + +```csharp +private static HybridAStarSearchResult CreateResult( + PlanningStatus status, + IEnumerable nodes, + int expandedNodeCount, + int generatedNodeCount, + int reopenedNodeCount, + int staleOpenListEntryCount, + int peakOpenListCount, + int? successNodeIndex, + string terminationReason = null) +{ + string reason = status == PlanningStatus.Success + ? string.Empty + : terminationReason ?? GetDefaultTerminationReason(status); + return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount, + staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason); +} + +private static string GetDefaultTerminationReason(PlanningStatus status) +{ + switch (status) + { + case PlanningStatus.Cancelled: + return "Hybrid A* 搜索已取消。"; + case PlanningStatus.InvalidRequest: + return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。"; + case PlanningStatus.InvalidMap: + return "Hybrid A* 搜索地图结构无效。"; + case PlanningStatus.MapNotReady: + return "Hybrid A* 搜索地图尚未准备好。"; + case PlanningStatus.InvalidVehicleParameters: + return "Hybrid A* 搜索车辆参数无效。"; + case PlanningStatus.InvalidCurvatureConfiguration: + return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。"; + case PlanningStatus.StartOutsideMap: + return "Hybrid A* 搜索起始扩大车体不完全位于地图内。"; + case PlanningStatus.StartInCollision: + return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。"; + case PlanningStatus.GoalOutsideMap: + return "Hybrid A* 搜索目标扩大车体不完全位于地图内。"; + case PlanningStatus.GoalInCollision: + return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。"; + case PlanningStatus.SearchTimeout: + return "Hybrid A* 搜索使用的总规划预算已耗尽。"; + case PlanningStatus.SearchNodeLimitExceeded: + return "Hybrid A* 搜索达到扩展节点上限。"; + case PlanningStatus.NoFeasiblePath: + return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"; + case PlanningStatus.BacktrackingFailed: + return "Hybrid A* 成功节点无法回溯为完整父链。"; + case PlanningStatus.FinalValidationFailed: + return "Hybrid A* 路径未通过最终复核。"; + case PlanningStatus.InternalError: + return "Hybrid A* 搜索发生未预期内部错误。"; + default: + return "Hybrid A* 搜索以未识别状态终止:" + status + "。"; + } +} +``` + +Change the two `NoFeasiblePath` exits so the caller can distinguish their location: + +```csharp +if (openList.Count == 0) + return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, + reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, + "二维启发式标记起点不可达目标,或起始方向无法进入 Open List。"); +``` + +```csharp +return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, + reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, + "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。"); +``` + +Replace the catch block with: + +```csharp +catch (Exception exception) +{ + string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, + reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, reason); +} +``` + +- [ ] **Step 5: Run the search script and verify GREEN** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +``` + +Expected: + +```text +Coarse path search primitive checks passed. +Coarse path Hybrid A star search checks passed. +``` + +- [ ] **Step 6: Commit the search reason contract** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/tests/verify_coarse_path_search.ps1 +git commit -m "fix: retain coarse path search failure reasons" +``` + +### Task 2: Add Planner-Level Diagnostics and Path Search Timing + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:75-135,189-210,357-360` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:6-60` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-226` + +**Interfaces:** +- Consumes: Task 1 `HybridAStarSearchResult.TerminationReason`. +- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`. +- Produces: `BuildSearchFailureReason(HybridAStarSearchResult, HybridAStarConfiguration) : string`. +- Contract: planner success and search-stage failure satisfy `0 <= PathSearchElapsed <= Elapsed`; map/preflight failure remains zero. + +- [ ] **Step 1: Add failing planner-diagnostic assertions** + +After the existing successful planner diagnostic assertions add: + +```powershell +Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) ` + 'Planner diagnostics must expose path-search elapsed time.' +Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` + 'Successful planning must retain non-negative path-search time.' +Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) ` + 'Path-search time must not exceed total elapsed time.' +``` + +After the invalid-map result assertions add: + +```powershell +Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed ` + 'Map failure must report zero path-search time.' +``` + +After creating a valid planner request, add a search-stage node-limit case: + +```powershell +$nodeLimitedRequest = [Activator]::CreateInstance($requestType) +$nodeLimitedRequest.Map = $request.Map +$nodeLimitedRequest.Start = $request.Start +$nodeLimitedRequest.Goal = $request.Goal +$nodeLimitedRequest.Vehicle = $request.Vehicle +$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType) +$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0 +$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2) +$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001 +$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001 +$nodeLimitedRequest.Configuration.AllowReverse = $false +$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward') +$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None)) +Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() ` + 'A zero node limit must fail after planner preflight.' +Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) ` + 'Search-stage node-limit failure must retain path-search time.' +Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) ` + 'Failed path-search time must not exceed total elapsed time.' +Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) ` + 'Planner diagnostics must retain a node-limit reason.' +``` + +Extend the no-path scenario assertions: + +```powershell +Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) ` + 'No-path diagnostics must retain the exact search exhaustion reason.' +Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('扩展=')) ` + 'No-path diagnostics must include search statistics.' +``` + +- [ ] **Step 2: Run integration checks and verify RED** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: build succeeds; the script fails first with `Planner diagnostics must expose path-search elapsed time.` + +- [ ] **Step 3: Extend `PlanningDiagnostics` compatibly** + +Append the optional constructor parameter: + +```csharp +TimeSpan pathSearchElapsed = default(TimeSpan) +``` + +Assign it after `Elapsed`: + +```csharp +Elapsed = elapsed; +PathSearchElapsed = pathSearchElapsed; +TerminationReason = terminationReason ?? string.Empty; +``` + +Add the property: + +```csharp +/// +/// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时; +/// 不含建图,搜索前失败时为零。 +/// +public TimeSpan PathSearchElapsed { get; } +``` + +Update the constructor XML comment so `elapsed` is described as total service elapsed time and `pathSearchElapsed` as the map-ready path-production elapsed time. + +- [ ] **Step 4: Start the path-search stopwatch at the exact boundary** + +Add: + +```csharp +using System.Diagnostics; +using System.Globalization; +``` + +Declare the stopwatch before the `try`: + +```csharp +Stopwatch pathSearchStopwatch = null; +``` + +Start it immediately before invoking the search: + +```csharp +pathSearchStopwatch = Stopwatch.StartNew(); +HybridAStarSearchResult searchResult = _search.Search(request, budget); +``` + +For `searchResult == null`, search failure, backtracking failure, assembly failure and final validation failure, pass `pathSearchStopwatch` into `Failure`. On success, pass its elapsed value: + +```csharp +return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, + pathLengthMeters, minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); +``` + +Replace the planner catch block with: + +```csharp +catch (Exception exception) +{ + string reason = "规划内部错误:" + exception.GetType().Name + + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); + return Failure(PlanningStatus.InternalError, + budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), + reason, null, pathSearchStopwatch); +} +``` + +- [ ] **Step 5: Compose actionable search diagnostics** + +For non-success search results, use: + +```csharp +if (searchResult.Status != PlanningStatus.Success) + return Failure(searchResult.Status, budget, + BuildSearchFailureReason(searchResult, request.Configuration), + searchResult, pathSearchStopwatch); +``` + +Add: + +```csharp +private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, + HybridAStarConfiguration configuration) +{ + string reason = string.IsNullOrEmpty(searchResult.TerminationReason) + ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" + : searchResult.TerminationReason; + string limit = string.Empty; + if (searchResult.Status == PlanningStatus.SearchTimeout) + { + limit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( + "F3", CultureInfo.InvariantCulture) + "s;"; + } + else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) + { + limit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( + CultureInfo.InvariantCulture) + ";"; + } + + return reason + limit + + "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + + "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + + "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; +} +``` + +Replace the two helper signatures and bodies: + +```csharp +private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, + HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) +{ + TimeSpan pathSearchElapsed = pathSearchStopwatch == null + ? TimeSpan.Zero + : pathSearchStopwatch.Elapsed; + return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, + reason, pathSearchElapsed)); +} + +private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, + double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) +{ + return new PlanningDiagnostics( + searchResult == null ? 0 : searchResult.ExpandedNodeCount, + searchResult == null ? 0 : searchResult.GeneratedNodeCount, + searchResult == null ? 0 : searchResult.ReopenedNodeCount, + searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, + searchResult == null ? 0 : searchResult.PeakOpenListCount, + pathLengthMeters, + minimumClearanceMeters, + elapsed, + reason, + pathSearchElapsed); +} +``` + +All search-preflight `Failure(...)` calls continue omitting the optional stopwatch and therefore report zero. All calls after `_search.Search` pass the running stopwatch. + +- [ ] **Step 6: Run integration checks and verify GREEN** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: + +```text +Coarse path integration checks passed. +Coarse path facade checks passed. +Coarse path P1 scenario checks passed. +``` + +- [ ] **Step 7: Commit planner diagnostics** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1 +git commit -m "feat: add actionable coarse path diagnostics" +``` + +### Task 3: Add Manual Timeout Input, UI Diagnostics, and Documentation + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:19-92` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:104-190,340-519` +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:114-116,285-359` + +**Interfaces:** +- Consumes: Task 2 `PlanningDiagnostics.PathSearchElapsed`. +- Produces: `ReadPositiveTimeoutInput(string) : TimeSpan`. +- Produces: `DrawStatus(string, CoarsePathPlanningJob, CoarsePathPlanningJobResult, PlanningGridMap)`. +- Contract: only the manual `[MovementTest(name = "粗路径规划")]` prompts for and overrides its request timeout; fixed scenarios keep their existing budgets. + +- [ ] **Step 1: Add failing UI and README source checks** + +After the current manual input assertions add: + +```powershell +Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' ` + 'The manual UI must read a finite positive timeout.' +Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' ` + 'The manual UI must apply the timeout to the current job.' +Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' ` + 'The manual timeout must convert seconds to TimeSpan.' +Assert-Match $source 'timeoutSeconds\s*<=\s*0' ` + 'The manual timeout must reject zero and negative values.' +Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) ` + 'The status layer and Toast must both show path-search elapsed time.' +Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' ` + 'The failure Toast must include the termination reason.' +Assert-Match $source 'ExpandedNodeCount' ` + 'The status layer must show expanded-node statistics.' +Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' ` + 'The status layer must show the effective turning radius.' +``` + +Append these entries to `$readmeStructure`: + +```powershell +'PathSearchElapsed', +'1.20 m', +'Reeds-Shepp', +``` + +- [ ] **Step 2: Run UI checks and verify RED** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: FAIL with `The manual UI must read a finite positive timeout.` + +- [ ] **Step 3: Read and apply one-shot manual timeout** + +Add: + +```csharp +using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; +``` + +In `CoarsePathPlanningTest.Test()`, read the timeout after goal heading and apply it after creating the job: + +```csharp +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); +``` + +Add: + +```csharp +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); + } +} +``` + +- [ ] **Step 4: Show timings, search counts, vehicle configuration, and failure reason** + +Change the call site to: + +```csharp +DrawStatus(scenarioName, job, result, map); +``` + +Replace `DrawStatus` with: + +```csharp +private static void DrawStatus(string scenarioName, CoarsePathPlanningJob job, + CoarsePathPlanningJobResult result, PlanningGridMap map) +{ + 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); + Painter.DrawText(Color.White, "地图:" + result.MapResult.Status + ",缓存:" + + result.MapResult.CacheHit + ",快照:" + snapshot, x, y + 120f); + 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); + Painter.DrawText(Color.White, "节点:扩展=" + + diagnostics.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + ",生成=" + + diagnostics.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + ",Open List峰值=" + + diagnostics.PeakOpenListCount.ToString(CultureInfo.InvariantCulture), x, y + 360f); + 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); + } + if (!string.IsNullOrEmpty(reason)) + Painter.DrawText(Color.LightYellow, "原因:" + reason, x, y + 600f); +} +``` + +Replace `BuildToastMessage` with: + +```csharp +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 + "。"; +} +``` + +- [ ] **Step 5: Document the exact manual-test boundaries** + +After “总预算与取消” add: + +```markdown +### 总耗时与路径搜索耗时 + +`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含 +地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed` 是地图和起终点预检 +通过后的路径搜索耗时,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核; +搜索开始前失败时为零。 +``` + +In “AMR 位姿、手动终点与障碍物”, add: + +```markdown +手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内 +的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖 +本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。 + +此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯 +半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。 +``` + +In “后台执行、停止与图层”, add: + +```markdown +状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、 +Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加 +`TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。 +``` + +Extend “第一版限制” with: + +```markdown +- Reeds-Shepp 或 Dubins 精确终点连接; +- 原地旋转。 + +当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、 +横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。 +``` + +- [ ] **Step 6: Run UI checks and verify GREEN** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: + +```text +Coarse path P1 UI source checks passed. +``` + +- [ ] **Step 7: Commit the manual-test UX** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1 +git commit -m "feat: expose coarse path test diagnostics" +``` + +### Task 4: Regress the Slow Feasible Case and Run the Full Suite + +**Files:** +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:273-369` +- Verify: all files modified by Tasks 1-3 + +**Interfaces:** +- Consumes: Task 3 manual scenario factory and configurable `SearchTimeout`. +- Produces: regression proof that the formerly 5-second-limited feasible pose succeeds under a caller-selected longer budget. +- Contract: the regression uses a 30 秒 upper bound without asserting an exact wall-clock duration. + +- [ ] **Step 1: Add the slow feasible-case regression** + +After the manual empty-map factory assertions, add: + +```powershell +$slowFeasibleJob = $factoryManual.Invoke($null, @( + 1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0)) +$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30) +$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000 +$slowFeasibleService = [Activator]::CreateInstance($serviceType) +$slowFeasibleResult = $servicePlan.Invoke( + $slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None)) +Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() ` + 'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.' +Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le + $slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) ` + 'Slow feasible path-search time must remain within total elapsed time.' +``` + +- [ ] **Step 2: Run the integration script and confirm the regression passes** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: + +```text +Coarse path integration checks passed. +Coarse path facade checks passed. +Coarse path P1 scenario checks passed. +``` + +The slow case is allowed up to 30 seconds and should normally complete near the observed 10-second baseline; do not assert an exact wall-clock value. + +- [ ] **Step 3: Run all coarse-path and map regressions** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: build exits 0 and every script prints its existing `passed` summary without an unhandled exception. + +- [ ] **Step 4: Inspect the final diff for scope and accidental edits** + +Run: + +```powershell +git diff -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_search.ps1 ClumsyPilot/tests/verify_coarse_path_integration.ps1 ClumsyPilot/tests/verify_coarse_path_ui.ps1 +``` + +Expected: only the approved reason propagation, timing, manual timeout, UI, documentation and tests are present; collision/search semantics and unrelated worktree files are unchanged. + +- [ ] **Step 5: Perform the Clumsy UI acceptance** + +Run `[MovementTest(name = "粗路径规划")]` with: + +```text +Start from current AMR pose corresponding to: 1000 mm, 2000 mm, 0 deg +Goal X: 1500 mm +Goal Y: 2500 mm +Goal heading: 90 deg +Timeout: 30 seconds +Manual obstacle count: 0 +``` + +Confirm: + +```text +Planning status: Success +Toast and status layer both show total elapsed and path-search elapsed +Status layer shows expanded/generated/Open List peak and the fixed demo vehicle parameters +``` + +Then run an intentionally impossible barrier or a deliberately short positive timeout and confirm the Toast includes a non-empty reason. + +- [ ] **Step 6: Commit the regression coverage** + +```powershell +git add -- ClumsyPilot/tests/verify_coarse_path_integration.ps1 +git commit -m "test: cover slow feasible coarse path planning" +``` diff --git a/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md b/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md new file mode 100644 index 0000000..f45f0b4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-coarsepath-readme-restructure.md @@ -0,0 +1,185 @@ +# CoarsePath README 结构化重构 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将 CoarsePath README 重构为与 Map README 相同的“结构—数据流—契约—最小示例—分步指南—常见错误”说明方式,同时保留准确的 P0/P1 边界。 + +**Architecture:** 保持所有生产代码不变。先为 README 的结构性事实增加稳定的 ASCII 文本断言,再将现有 README 的正确内容重组为面向调用者的模块说明,最后运行文档、构建与集成回归,证明这只是文档交付。 + +**Tech Stack:** Markdown、PowerShell、.NET `netstandard2.0` Debug 构建、现有 CoarsePath 验证脚本。 + +## Global Constraints + +- 只修改 `CoarsePath/README.md` 与其文档断言;不得改动 Map、CoarsePath、P1 UI 或测试场景的运行行为。 +- README 只陈述当前已实现并经自动化验证的 P0/P1 能力;实际 Clumsy 的人工视觉验收仍要明确为待执行。 +- 业务调用示例固定使用 `CoarsePathPlanningService.Plan(job, cancellationToken)`;不得鼓励 UI 或调用方直接拼接搜索组件。 +- Map 障碍物投影、栅格化和缓存细节只链接到 `../Map/README.md`,不复制为 CoarsePath 实现说明。 +- 坐标说明必须保持:Map 输入为 mm,核心位姿/路径为 m,核心航向为 rad;P1 UI 的 AMR 输入航向为 deg 并在边界转换。 +- 显式空图只能描述为 P1 单位/可视化演示,不能描述为真实作业地图。 +- 不恢复、清理或迁移 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。 + +--- + +## 文件结构 + +| 文件 | 修改职责 | +| --- | --- | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 重组现有 P0/P1 内容,加入实际目录树、规划数据流、分步指南与常见错误。 | +| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 用 ASCII 关键字保护 README 的结构、核心边界和 P1 说明。 | + +### Task 1: 为 README 重构建立失败的结构断言 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` +- Verify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` + +**Consumes:** 现有 `$readmePath`、`$readme`、`Assert-True` 及 P1 UI 源码检查。 + +**Produces:** 文档结构保护;README 缺少新的 Map 风格章节或 P1 边界时脚本失败。 + +- [ ] **Step 1: 在现有 README 断言后加入目标结构的失败检查** + +在当前 `$requiredText` 循环之后插入以下 PowerShell。所有匹配项保持 ASCII,避免 Windows PowerShell 无 BOM 脚本中的中文编码差异: + +```powershell +$readmeStructure = @( + 'File Structure', + 'Planning Data Flow', + 'Build Status and Stop', + 'Coordinates and Units', + 'Minimal Call Example', + 'Cache and SourceVersion', + 'Detailed Usage Guide', + 'P1 Manual Tests and Visualization', + 'Common Errors', + 'First-Version Limits', + 'CoarsePathPlanningService.Plan(job, cancellationToken)', + 'CoarsePathPlanningJob', + 'PlanningGridMap', + 'SourceVersion', + 'CoarsePathPlanningV1', + 'CancellationTokenSource', + 'NoFeasiblePath', + 'IsGearSwitchPoint', + '../Map/README.md' +) +foreach ($requiredText in $readmeStructure) { + Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText." +} +``` + +- [ ] **Step 2: 运行脚本确认 README 仍缺少新结构** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `Restructured CoarsePath README must document File Structure.`;源码 UI 断言仍通过。 + +### Task 2: 重构 CoarsePath README 的模块说明与调用文档 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Test: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` + +**Consumes:** Map README 的组织方式;现有 CoarsePath README 的真实 P0/P1 契约;`CoarsePathPlanningService.Plan(job, cancellationToken)`。 + +**Produces:** 一份可从零开始阅读的 CoarsePath 模块说明,内容与当前实现一致。 + +- [ ] **Step 1: 用 Map 风格的顶层章节替换现有 README 的章节顺序** + +保留 README 标题 `# CoarsePath 粗路径规划(P0/P1)`,然后按以下顺序重新组织内容;将每个二级标题同时写为中文说明和括号中的 ASCII 稳定标识,例如 `## 文件结构(File Structure)`,使人类读者与 Task 1 断言都能使用: + +1. `## 模块说明(Module Overview)`:说明 Map 提供只读快照,CoarsePath 输出已复核的粗路径;唯一业务入口是 `CoarsePathPlanningService.Plan(job, cancellationToken)`;列出不负责的控制、速度、实时重规划等职责。 +2. `## 文件结构(File Structure)`:使用 `text` 目录树列出实际 `Contracts/`、`Vehicle/`、`Search/`、`Output/`、`Facade/` 与 `Test/` 文件,逐项写出与当前目录对应的职责。 +3. `## 规划数据流(Planning Data Flow)`:画出 `CoarsePathPlanningJob -> CoarsePathPlanningService -> PlanningMapFactory.Create -> PlanningGridMap -> HybridAStarPlanner -> PlanningResult -> CoarsePathPlanningJobResult`;在失败分支注明地图失败不启动搜索。 +4. `## 构建状态与停止(Build Status and Stop)`:说明 `MapResult` 和 `PlanningResult` 必须一起处理,解释 `Success`、`Cancelled`、`SearchTimeout`、`NoFeasiblePath` 与空路径规则。 +5. `## 坐标与单位(Coordinates and Units)`:用表格列出地图 mm、`Pose2D`/路径 m、核心航向 rad、P1 AMR 输入 deg;明确起点为车身几何中心和安全余量由 `VehicleParameters.SafetyMarginMeters` 表达。 +6. `## 最小调用示例(Minimal Call Example)`:保留并精简当前服务调用示例;包含 `PlanningMapRequest`、`Pose2D`、`VehicleParameters`、`HybridAStarConfiguration`、`MapResult` 与 `PlanningResult` 的失败处理。 +7. `## 缓存与 SourceVersion(Cache and SourceVersion)`:说明服务长期存活、`Input`/`Occupancy`/`None` 缓存层级,及来源内容变更必须递增 `SourceVersion`。 +8. `## 详细使用指南(Detailed Usage Guide)`:用六步小节解释长期服务、准备地图请求、填写起终点、填写车辆、调整搜索配置、调用及消费路径/方向段;链接 `../Map/README.md` 说明障碍物来源和栅格化。 +9. `## P1 手动测试与可视化(P1 Manual Tests and Visualization)`:包含七个 MovementTest 的场景表、`CoarsePathPlanningTest` 的 `getCartLocation`/手动目标转换、`CancellationTokenSource`/`Task.Run`/`TestStop` 停止语义、`CoarsePathPlanningV1` 图层及颜色图例。明确人工视觉验收尚待在实际 Clumsy 中执行。 +10. `## 常见错误(Common Errors)`:以“现象 / 原因 / 处理”表格写入:mm 当作 m、deg 当作 rad、`SourceVersion` 未递增、隐式空图、未处理非成功结果、把粗路径当作底盘可执行轨迹。 +11. `## 第一版限制(First-Version Limits)`:保留并归并路径平滑、速度/时间轨迹、底盘控制、实时重规划、真实作业地图、Release 基准等明确非目标。 + +- [ ] **Step 2: 对照实际目录和 P1 实现,校验每个文件树项与说明的真实性** + +确认目录树只引用下列已存在组件: + +```text +Contracts/: Pose2D, PlanningRequest, PlanningResult, PlanningStatus, + CoarsePathPoint, PathSegment, VehicleParameters, HybridAStarConfiguration +Vehicle/: VehicleKinematics, VehicleFootprint, FootprintCollisionChecker, + OrientedRectangleCellIntersection +Search/: BinaryMinHeap, GridDijkstraHeuristic, GoalToleranceChecker, + MotionPrimitive, MotionPrimitiveGenerator, SearchCostCalculator, + HybridAStarNode, HybridAStarNodeKey, HybridAStarSearch +Output/: PathBacktracker, CoarsePathAssembler, CoarsePathValidator +Facade/: CoarsePathPlanningJob, CoarsePathPlanningJobResult, + CoarsePathPlanningService, PlanningDebugOptions, IPlanningDebugSink +Test/: CoarsePathScenarioFactory, MovementTest.CoarsePathTest +``` + +不要在 README 中承诺不存在的平滑器、控制器、实时数据源或 Release 基准。 + +- [ ] **Step 3: 运行文档结构检查确认通过** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `Coarse path P1 UI source checks passed.` + +### Task 3: 验证文档重构没有影响 P0/P1 行为 + +**Files:** + +- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Verify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` +- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Tasks 1–2 的 README 与断言。 + +**Produces:** 从最终工作区获得的文档、构建和集成验证证据。 + +- [ ] **Step 1: 构建项目** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +``` + +Expected: `0 个错误`;允许项目已有的两条过时 API 警告。 + +- [ ] **Step 2: 运行 P1 文档/UI 结构检查** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `Coarse path P1 UI source checks passed.` + +- [ ] **Step 3: 运行粗路径集成回归** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 依次输出 `Coarse path integration checks passed.`、`Coarse path facade checks passed.` 与 `Coarse path P1 scenario checks passed.`。 + +## 自检 + +- **规格覆盖:** Task 2 覆盖设计中的十个 README 章节、P0/P1 已完成边界、Map 链接、单位、空图限制与人工验收状态;Task 1 保护可自动检查的结构事实;Task 3 给出最终证据。 +- **完整性检查:** 本计划不含未决实现、泛化错误处理或未命名的验证步骤;每项改动均有文件路径、具体内容与命令。 +- **一致性:** 所有调用名、状态名、场景工厂、P1 图层和坐标单位均与现有 CoarsePath 代码一致;计划不引入新 C# 接口或依赖。 diff --git a/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md b/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md new file mode 100644 index 0000000..52742ea --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-p1-manual-obstacle-input.md @@ -0,0 +1,365 @@ +# P1 手动障碍物输入 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让“粗路径规划”MovementTest 支持一次输入最多 20 个圆形或轴对齐矩形障碍物,并通过既有门面规划、快照绘制和取消流程验证结果。 + +**Architecture:** 纯几何输入和 Map 请求构造保留在 `CoarsePathScenarioFactory`,UI 只读取、验证和冻结操作者输入。每次含障碍物的手动运行由共享执行器颁发单调递增快照版本,保证 Map 缓存不会错误复用旧障碍物;Painter 继续只读取最终 `PlanningGridMap`。 + +**Tech Stack:** C# / `netstandard2.0`、现有 `ManualObstacleSource`、Clumsy `MovementTest`/`UI.GetInput`、PowerShell 反射与源码验证脚本。 + +## Global Constraints + +- 不改变 `CoarsePathPlanningService.Plan(job, token)` 作为唯一业务规划入口的边界;UI 不得直接创建地图工厂、搜索器、碰撞器或原语。 +- 手动输入的 X/Y、圆半径和矩形长宽全部使用世界 mm;AMR/目标航向输入使用 deg;核心 `Pose2D` 使用 m/rad。 +- 障碍物数量范围固定为 0–20;圆半径、矩形 X 长度和 Y 宽度必须是有限正数;矩形始终与世界坐标轴平行。 +- 有障碍物时使用必需的 `ManualObstacleSource("manual-user-input", version, true, ...)` 且关闭显式空图;零障碍物时才允许显式空图。 +- 手动地图边界必须覆盖起点、终点及每个障碍物完整外轮廓,再保留 2000 mm 留白并按 50 mm 向外取整。 +- 含障碍物手动提交必须使用单调递增快照版本;固定场景的缓存命中行为不得改变。 +- 保留后台 `Task.Run`、`CancellationTokenSource`、`TestStop`、结果快照绘制和无底盘命令边界。 +- 不支持旋转矩形、多边形、文件导入、拖拽编辑或运行中修改障碍物;不执行 Git 操作。 + +--- + +## 文件结构 + +| 文件 | 修改职责 | +| --- | --- | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新增手动障碍物纯数据类型、工厂方法、几何校验、动态边界和来源快照构造。 | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 为“粗路径规划”读取数量、类型、中心和尺寸,生成单调来源版本并提交工厂请求。 | +| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 通过程序集反射验证工厂、障碍来源、空图分支、几何边界和无效尺寸。 | +| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 验证 UI 入口包含手动障碍物输入与工厂调用,同时保持无直接地图/搜索依赖。 | +| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 补充手动障碍物的输入顺序、单位、上限、矩形方向和空图限制。 | + +### Task 1: 手动障碍物工厂契约与行为验证 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` +- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` + +**Consumes:** 现有 `$assembly`、`$testNamespace`、`$scenarioFactoryType`、`Find-Method`、`Assert-True`、`Assert-Equal`、`Assert-Near`。 + +**Produces:** `ManualCoarsePathObstacleKind`、`ManualCoarsePathObstacle` 和 `CreateManualObstacleDemo` 的反射/行为契约。 + +- [ ] **Step 1: 在 P1 工厂断言后加入失败的手动障碍物检查** + +在 `$manualJob` 的现有断言之后插入下面代码。它使用数组传入 `IReadOnlyList`,并检查请求尚未存在时的类型/方法失败。 + +```powershell +$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false) +$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false) +Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.' +Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.' + +$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double]) +$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double]) +$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object { + $_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8 +} | Select-Object -First 1 +Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.' +Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.' +Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.' + +$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2) +$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0) +$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1) +$manualObstacleJob = $manualObstacleFactory.Invoke($null, @( + 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77)) +Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.' +Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.' +Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.' +Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.' +Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.' +Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.' + +$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0) +$emptyManualJob = $manualObstacleFactory.Invoke($null, @( + 1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0)) +Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.' +Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.' + +try { + $null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0)) + throw 'Zero-radius manual circle must be rejected.' +} +catch [Reflection.TargetInvocationException] { + Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) 'Invalid manual geometry must report argument range.' +} +``` + +- [ ] **Step 2: 构建并运行脚本确认新契约失败** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建成功;脚本报出 `Manual obstacle kind enum must exist.`。 + +- [ ] **Step 3: 在场景工厂实现不可变手动障碍物类型** + +在 `CoarsePathScenarioFactory.cs` 的固定场景枚举之后加入如下公共类型。构造函数保持私有,强制圆形与矩形分别通过语义明确的静态工厂创建;所有几何输入均为 mm。 + +```csharp +/// 手动障碍物的支持几何类型。 +public enum ManualCoarsePathObstacleKind +{ + /// 由圆心和半径定义的圆形障碍物。 + Circle, + /// 由几何中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。 + AxisAlignedRectangle, +} + +/// 手动粗路径测试的不可变障碍物输入;全部几何数据使用世界 mm。 +public sealed class ManualCoarsePathObstacle +{ + private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters, + double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters) + { + Kind = kind; CenterXMillimeters = centerXMillimeters; CenterYMillimeters = centerYMillimeters; + SizeXMillimeters = sizeXMillimeters; SizeYMillimeters = sizeYMillimeters; + } + + public ManualCoarsePathObstacleKind Kind { get; } + public double CenterXMillimeters { get; } + public double CenterYMillimeters { get; } + public double SizeXMillimeters { get; } + public double SizeYMillimeters { get; } + + public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters, + double radiusMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters, + centerYMillimeters, radiusMillimeters, radiusMillimeters); + } + + public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters, + double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters) + { + EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); + EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); + EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters)); + EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters)); + return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle, + centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters); + } +} +``` + +将 `EnsureFinite` 与新增 `EnsurePositiveFinite` 定义为可被同一命名空间类型调用的内部静态校验辅助方法,或在 `ManualCoarsePathObstacle` 中实现等价私有辅助方法;无效值必须抛出 `ArgumentOutOfRangeException`。 + +- [ ] **Step 4: 实现手动障碍物请求和动态边界** + +在 `CoarsePathScenarioFactory` 加入下面公共方法,并让现有 `CreateManualGoalDemo` 调用它的零障碍物分支,以保留当前空图契约: + +```csharp +public static CoarsePathPlanningJob CreateManualObstacleDemo( + double startXMillimeters, double startYMillimeters, double startHeadingDegrees, + double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees, + IReadOnlyList obstacles, long obstacleSnapshotVersion) +{ + ValidateManualPoseInputs(startXMillimeters, startYMillimeters, startHeadingDegrees, + goalXMillimeters, goalYMillimeters, goalHeadingDegrees); + IReadOnlyList items = obstacles ?? + throw new ArgumentNullException(nameof(obstacles)); + if (items.Count > MaximumManualObstacleCount) + throw new ArgumentOutOfRangeException(nameof(obstacles)); + if (items.Count == 0) + return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, + goalXMillimeters, goalYMillimeters, Array.Empty()), + ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), + ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); + if (obstacleSnapshotVersion <= 0) + throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion)); + + IMapObstacle[] mapObstacles = ConvertManualObstacles(items); + IMapObstacleSource[] sources = + { + new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, mapObstacles), + }; + return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, + goalXMillimeters, goalYMillimeters, items), + ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), + ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); +} +``` + +Use `CreateManualMapRequest(items, sources)` rather than leaving the above source array unused: it must create a `PlanningMapRequest` with the dynamic bounds, `ResolutionMm = 50f`, those sources and `AllowExplicitEmptyMap = false`. `ConvertManualObstacles` must map a circle to `new CircleObstacle(centerX, centerY, radius)` and a rectangle to `new AxisAlignedRectangleObstacle(centerX - lengthX / 2, centerX + lengthX / 2, centerY - widthY / 2, centerY + widthY / 2)` after range-safe float conversion. + +Refactor `CreateManualDemoMap` to accept an obstacle collection and include its circle/rectangle extents before adding 2000 mm padding and applying `ToGridLowerBound`/`ToGridUpperBound`. Zero obstacles must retain `Array.Empty()` and `AllowExplicitEmptyMap = true`. + +- [ ] **Step 5: 运行工厂行为检查确认通过** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建成功;输出既有三行集成通过信息,且手动圆/矩形、空障碍物和无效半径断言均通过。 + +### Task 2: MovementTest 逐项输入与来源版本 + +**Files:** + +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` +- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` + +**Consumes:** Task 1 的 `ManualCoarsePathObstacle.Circle`、`ManualCoarsePathObstacle.AxisAlignedRectangle` 与 `CoarsePathScenarioFactory.CreateManualObstacleDemo`。 + +**Produces:** `CoarsePathPlanningTest` 在启动规划前读取并冻结最多 20 个手动障碍物,随后使用递增版本提交给工厂。 + +- [ ] **Step 1: 加入失败的 UI 源码边界断言** + +在现有手动工厂断言后加入: + +```powershell +Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.' +Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.' +Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.' +Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.' +Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.' +Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.' +Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.' +``` + +- [ ] **Step 2: 运行 UI 脚本确认新断言失败** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `The manual UI must construct typed manual obstacles.`。 + +- [ ] **Step 3: 实现输入辅助方法与提交逻辑** + +在 `CoarsePathPlanningTest` 中新增: + +```csharp +private const int MaximumManualObstacleCount = 20; +private static long _nextManualObstacleSnapshotVersion; + +private static IReadOnlyList ReadManualObstacles() +{ + int count = ReadBoundedIntegerInput("手动障碍物数量(0-20)", 0, MaximumManualObstacleCount); + var obstacles = new List(count); + for (int index = 0; index < count; index++) + { + int kind = ReadBoundedIntegerInput("障碍物 " + (index + 1) + " 类型(1圆形,2矩形)", 1, 2); + double centerX = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 X(世界 mm)"); + double centerY = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 Y(世界 mm)"); + if (kind == 1) + { + double radius = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " 半径 r(mm)"); + obstacles.Add(ManualCoarsePathObstacle.Circle(centerX, centerY, radius)); + } + else + { + double lengthX = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " X方向长度(mm)"); + double widthY = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " Y方向宽度(mm)"); + obstacles.Add(ManualCoarsePathObstacle.AxisAlignedRectangle(centerX, centerY, lengthX, widthY)); + } + } + return obstacles; +} +``` + +`ReadBoundedIntegerInput` 复用 `UI.GetInput` 和当前文化/InvariantCulture 解析,拒绝非整数或超出 `[minimum, maximum]` 的输入;`ReadPositiveFiniteInput` 在 `ReadFiniteInput` 返回后拒绝 `<= 0d`。所有失败继续由现有 `ShowInputFailure` 显示。 + +在 `Test()` 中的终点读取后调用 `ReadManualObstacles()`。当集合非空时,用 `Interlocked.Increment(ref _nextManualObstacleSnapshotVersion)` 取得版本;集合为空时使用 `0L`。随后替换现有工厂调用: + +```csharp +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); +CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job); +``` + +保留 `TestStop`、`Run`、Painter 和底盘禁止边界,不在 UI 内构造 `ManualObstacleSource`、`PlanningMapRequest` 或搜索对象。 + +- [ ] **Step 4: 运行 UI 结构检查确认通过** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `Coarse path P1 UI source checks passed.` + +### Task 3: README 输入说明与最终回归 + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` +- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` +- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` + +**Consumes:** Tasks 1–2 的工厂与 UI 输入契约。 + +**Produces:** README 中与实际输入顺序一致的手动障碍物说明,以及最终的构建、UI 和集成证据。 + +- [ ] **Step 1: 为 README 增加失败的 ASCII 文档断言** + +在 `$readmeStructure` 的数组中加入: + +```powershell +'CreateManualObstacleDemo', +'ManualCoarsePathObstacle', +'manual-user-input', +'0-20', +'AxisAlignedRectangle', +``` + +- [ ] **Step 2: 运行 UI 脚本确认 README 断言失败** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +``` + +Expected: `Restructured CoarsePath README must document CreateManualObstacleDemo.` + +- [ ] **Step 3: 更新 README 的 P1 手动测试段落** + +在 `## P1 手动测试与可视化(P1 Manual Tests and Visualization)` 的“AMR 位姿与手动终点”小节中,替换“空图入口”的单一说明,加入以下事实: + +1. 目标输入之后先输入 `0-20` 的障碍物数量; +2. 每项输入 `1` 圆形或 `2` 矩形、中心 X/Y(mm),圆形半径或矩形 X 长度/Y 宽度(mm); +3. 矩形是 `AxisAlignedRectangle`,不支持旋转;尺寸必须为正; +4. `CreateManualObstacleDemo` 将它们包装为 `manual-user-input` 快照,有障碍物时关闭显式空图; +5. 零障碍物才是坐标/取消演示的显式空图;真实作业仍必须提供真实障碍物来源; +6. 地图边界自动覆盖起终点和障碍物完整外轮廓,保留 2000 mm 留白并按 50 mm 对齐; +7. 每次含障碍物提交使用新版本,Painter 仍显示最终 `PlanningGridMap` 占据格而不是原始几何。 + +- [ ] **Step 4: 运行完整验证** + +Run: + +```powershell +dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 +``` + +Expected: 构建为 `0 个错误`;UI 脚本输出 `Coarse path P1 UI source checks passed.`;集成脚本依次输出既有三行 `passed` 消息。 + +## 自检 + +- **规格覆盖:** Task 1 覆盖几何类型、非空/空地图、动态边界、版本和无效尺寸;Task 2 覆盖 0–20 输入、形状输入、版本和后台门面边界;Task 3 覆盖 README 与回归。 +- **完整性检查:** 每个实现步骤指定了文件、调用签名、验证规则和命令;不引入未命名接口或外部依赖。 +- **一致性:** `ManualCoarsePathObstacle`、`CreateManualObstacleDemo`、`manual-user-input` 与 `obstacleSnapshotVersion` 在所有任务中使用相同名称和单位定义。 diff --git a/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md b/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md new file mode 100644 index 0000000..e9a96f6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-path-smoothing-six-figure-report.md @@ -0,0 +1,73 @@ +# Path smoothing six-figure report implementation plan + +> **Execution:** Implement in this workspace without staging or committing. The worktree contains unrelated user changes; touch only the path-smoothing report code, its tests, and its documentation. + +**Goal:** Replace each scenario's legacy composite `comparison.svg/png` output with six focused SVG/PNG figures and one CSV, using discrete trajectory samples only (no path-connecting strokes). + +**Architecture:** Keep `SmoothingFigureModel` as the immutable source data extracted from comparison results. Add a figure-set layer that selects series, camera bounds, map decorations, axis configuration, and title/legend per output figure. Both renderers consume that same figure definition, so SVG and PNG communicate exactly the same data. The exporter creates all twelve images and the CSV in temporary sibling files, then publishes the completed set and removes legacy composite images. + +**Technology:** C#/.NET 10 (`System.Drawing.Common` for PNG); hand-authored SVG; existing PowerShell verification harness and `PathSmoothingPngVerificationHost`. + +--- + +## Task 1: Define six figure views from the common report model + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` +- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` + +1. Extend the C# verification host first with assertions for six ordered figure kinds/stems, selected series, equal-scale world bounds, labels with units, and point-only series metadata. Run the host and confirm it fails because no figure set exists. +2. Remove `DashArray` as a trajectory styling contract from `SmoothingFigureSeries` and legend entries. Preserve source points, status, colors, raw baseline flag, violations, map obstacles, start, goal, and metric rows. +3. Implement immutable figure definitions with fixed stems: + - `01-coarse-path-overview`: raw only; map obstacles and start/goal. + - `02-all-paths-comparison`: raw plus all three smoothing methods; paths and axes/legend only. + - `03-cubic-bspline-overview`, `04-local-cubic-bezier-overview`, `05-piecewise-quintic-overview`: faded raw reference plus the named method; map obstacles and start/goal. + - `06-curvature-comparison`: raw plus all smoother curvature samples. +4. Compute a trajectory-driven world view for each overhead figure: union only visible series points plus its relevant start/goal, add 10% padding with a 0.25 m minimum extent, and expand the smaller world dimension so projected X and Y scale are equal. Do not use full map bounds to zoom out a figure. +5. Include deterministic “nice” axis ticks/labels in metres for overhead figures and arc length/curvature units for the final figure. Preserve failed/infeasible method labels in legends even when their geometry has no points. +6. Rerun the host checks; expected result: it passes definition-level checks while renderer-output checks remain to be updated in Tasks 2–3. + +## Task 2: Render six focused point-cloud figures and publish the set + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs` +- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` + +1. Add output-level tests in the verification host for the twelve exact image names, the single CSV, non-empty parseable SVGs, readable 600 dpi PNGs, and absence of temporary files. Run them and confirm the legacy one-image exporter fails these expectations. +2. Refactor the SVG renderer to render one figure definition at a time. Draw axes, ticks, numeric labels, unit labels, legend point swatches, map rectangles (when requested), and start/goal markers. Draw every trajectory and curvature sample as a small marker; do not emit a trajectory ``, polyline, dash array, or line segment. +3. Apply the identical layout semantics in the PNG renderer. Draw points rather than calling a line-drawing API for path samples; give raw reference samples a reduced alpha in individual smoother figures. Keep 600 dpi metadata and the existing required-font behavior. +4. Refactor the exporter to build the six definitions and write twelve temporary image files plus the CSV before publishing. Return collections of SVG and PNG paths with the one CSV path. Delete `comparison.svg/png` after a successful new-set publish; on failure, clean temporary files and retain existing published outputs. +5. Update demo/host call sites from singular `SvgPath`/`PngPath` to the path collections. Run the verification host; expected result: six SVGs, six PNGs, and CSV are all present and valid. + +## Task 3: Update external verification, runner documentation, and visually inspect outputs + +**Files:** +- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` +- Modify: `ClumsyPilot/tests/run_path_smoothing_comparison.ps1` (only if it states/assumes legacy filenames) +- Modify: `docs/superpowers/specs/2026-07-30-path-smoothing-six-figure-report-design.md` (only if implementation exposes a necessary clarified contract) + +1. Update PowerShell tests to assert exactly six SVG + six PNG filenames, one CSV, no legacy composite output, required unit labels, marker-based trajectory rendering, and no trajectory dash/line styles. Ensure test source uses safe UTF-8 handling rather than brittle localized literal matching. +2. Update the README to document the six filenames, marker-only semantics, method statuses, coordinate units, and the one-command runner output structure. +3. Run the focused report verification scripts and the PNG host. Regenerate at least one fixture report with the current 0.025 m smoothing output sampling. +4. Render/open representative PNGs for visual QA: raw overview, all-path overlay, each individual smoother, and curvature. Check that curves fill the frame, coordinates/units are legible, all points are visible, individual figures retain context, and there are no joined path lines. +5. Run `dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore` and relevant contract/service/integration tests. Confirm `git diff --check` and report exact files changed; do not stage or commit. + +## Acceptance checklist + +- Each scenario produces exactly `01` through `06` SVGs and corresponding PNGs plus one CSV. +- Raw and smoothed trajectories use every sampled point and zero connecting lines. +- Overhead figures use equal X/Y scale, trajectory-focused bounds, numeric axes, and metre units. +- Curvature uses `s (m)` and `κ (m⁻¹)` axes with a complete legend and statuses. +- SVG and PNG agree on the six figure contents, fonts, units, colors, and point-only semantics. +- Every PNG is 600 dpi; failed export leaves no temporary files or partial newly generated set. diff --git a/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md b/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md new file mode 100644 index 0000000..c30985d --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md @@ -0,0 +1,249 @@ +# Local G2 Dailywork Reports Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `dailywork_report/` 中交付两份清晰、可追溯的 Local G2 五次 Hermite 中文报告,并为每份提供一个可离线打开的 HTML 可视化附录。 + +**Architecture:** 报告按“算法事实”和“问题证据”拆分,避免把候选层能力误写成已完成的端到端功能。每份 HTML 均为独立单文件,以内嵌 CSS、SVG 和少量原生 JavaScript 将 Markdown 的核心结构可视化;不引入构建工具或外部资源。 + +**Tech Stack:** Markdown、HTML5、内嵌 CSS、内嵌 SVG、原生 JavaScript、PowerShell 验证。 + +## Global Constraints + +- 目录根为 `dailywork_report/`,大小写和下划线必须保持一致。 +- 建立 `Map_rep/` 与 `coarsepath_rep/` 作为空的未来报告入口;本次不填充其业务内容。 +- 本次四份正式内容只放在 `dailywork_report/pathsmoothing_rep/`。 +- 所有文字使用中文;首次出现的英文技术术语必须有中文解释或可由相邻中文短语理解。 +- 明确区分:专项测试通过、已复现故障、静态分析确认的逻辑缺口、待验证集成风险。 +- 不修改任何路径平滑、地图、粗路径或测试实现。 +- HTML 不使用 CDN、网络请求、第三方库、外部图片或构建步骤。 +- 工作区已有无关改动;本任务不暂存、不提交。 + +--- + +### Task 1: 建立稳定的日报目录边界 + +**Files:** +- Create: `dailywork_report/Map_rep/.gitkeep` +- Create: `dailywork_report/coarsepath_rep/.gitkeep` +- Create: `dailywork_report/pathsmoothing_rep/`(由后续两个任务创建内容) + +**Interfaces:** +- Consumes: 已批准的 `docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md`。 +- Produces: 可承载地图、粗路径和路径平滑报告的稳定目录边界。 + +- [ ] **Step 1: 创建两个空模块目录的保留文件** + +使用 `apply_patch` 创建两个空的 `.gitkeep` 文件,内容保持为空: + +```text +dailywork_report/Map_rep/.gitkeep +dailywork_report/coarsepath_rep/.gitkeep +``` + +- [ ] **Step 2: 验证目录边界** + +运行: + +```powershell +$paths = @( + 'dailywork_report/Map_rep/.gitkeep', + 'dailywork_report/coarsepath_rep/.gitkeep' +) +foreach ($path in $paths) { + if (-not (Test-Path -LiteralPath $path)) { throw "Missing report directory marker: $path" } +} +Write-Output 'Dailywork report directory checks passed.' +``` + +预期:输出 `Dailywork report directory checks passed.`。 + +--- + +### Task 2: 编写 Local G2 算法主报告及流程可视化附录 + +**Files:** +- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md` +- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` + +**Interfaces:** +- Consumes: `docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md`、`docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md`、`ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/` 下的实现。 +- Produces: 一份说明 Local G2 候选层工作方式、输入、输出、约束和当前接入边界的报告;一份与该报告事实一致的可视化附录。 + +- [ ] **Step 1: 写入 Markdown 主报告的固定章节** + +按下列一级标题顺序撰写,并在每节中给出可核对的事实: + +```markdown +# Local G2 五次 Hermite 路径平滑算法说明 +## 1. 目标、位置与非目标 +## 2. 输入:进入算法前必须具备什么 +## 3. 模块架构:每个模块负责什么 +## 4. 数据流:从粗路径到候选安全路径 +## 5. 输出:路径、段、指标、区域报告与状态 +## 6. 安全与质量门 +## 7. 当前实现进度与边界 +``` + +必须写清:输入为成功的 Hybrid A* 粗路径、路径方向段、地图、车辆参数、`LocalG2QuinticOptions`、取消令牌;距离单位为米、航向为弧度、曲率为 `1/m`。数据流必须依次解释预处理、曲率跳变检测、窗口规划、五次 Hermite 候选构造、局部拼接、统一几何分析、完整车体验证、质量评价。输出必须解释 `SmoothedPathPoint`、`SmoothedPathSegment`、曲率 `κ`、曲率导数 `dκ/ds`、区域报告和诊断。 + +“当前实现进度与边界”必须明确:任务 1–7 已达到候选构造与评价层;`LocalG2PreSmoothingPipeline` 与服务分派尚未实现,因此不能声称目前可正式发布完整的 Local G2 SQP 初始路径。 + +- [ ] **Step 2: 写入单文件 HTML 算法附录** + +HTML 必须包含 `
`、一个“输入”卡片区、一个按顺序排列的 SVG 流程图、一个“输出”卡片区和一个“当前边界”提示区。SVG 流程节点必须使用以下稳定文字: + +```text +Hybrid A* 粗路径 +预处理与方向分段 +曲率跳变检测 +窗口规划 +五次 Hermite 候选 +拼接与统一几何分析 +完整车体安全与质量评价 +计划中的发布流水线(尚未接入) +``` + +用绿色标记已实现的候选层节点,用琥珀色标记“尚未接入”的发布流水线节点。HTML 中的“输入”和“输出”文字必须与 Markdown 报告一致,且页面顶部必须写明“离线静态可视化附录”。 + +- [ ] **Step 3: 校验主报告与 HTML 的算法事实** + +运行: + +```powershell +$markdown = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md' +$html = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html' +$markdownTerms = @('输入:进入算法前必须具备什么', '输出:路径、段、指标、区域报告与状态', 'dκ/ds', '尚未实现') +$htmlTerms = @(']+src=') { throw 'HTML appendices must be self-contained.' } +Write-Output 'Dailywork report package checks passed.' +``` + +预期:输出 `Dailywork report package checks passed.`。 + +- [ ] **Step 2: 在本地浏览器进行人工可读性审阅** + +依次打开两个 HTML 文件,检查以下具体条件: + +```text +算法附录:流程从左到右或从上到下可顺序阅读;绿色已实现节点与琥珀色未接入节点容易区分;输入和输出没有被流程图遮挡。 +问题附录:三类证据徽章颜色和文字均可分辨;三个问题卡片的例子、成因、措施没有被截断;行动顺序位于页面末尾且与 Markdown 一致。 +``` + +- [ ] **Step 3: 检查工作区改动范围** + +运行: + +```powershell +git diff --check +git status --short -- dailywork_report docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md +``` + +预期:无空白错误;改动只包含本计划的设计、计划和日报交付文件。 diff --git a/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md b/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md new file mode 100644 index 0000000..2fd0de6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-local-g2-interactive-visualization-redesign.md @@ -0,0 +1,404 @@ +# Local G2 Interactive Visualization Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将两份 Local G2 HTML 报告附录从流程卡片重做为可逐步查看路径变化、并能对照错误与正确预期的离线交互式可视化。 + +**Architecture:** 两页均继续为独立 HTML 文件。算法页使用一个固定坐标系的 SVG 和六个可切换状态,曲线由内嵌 JavaScript 的五次 Hermite 基函数计算并绘制;问题页使用三个可切换的 SVG 场景,每个场景同时呈现“实际发生 / 正确应有 / 差异原因”。所有教学几何固定标为典型示例,真实测试结论只以已知状态与原始文本呈现。 + +**Tech Stack:** HTML5、CSS、内嵌 SVG、原生 JavaScript、PowerShell 静态验证。 + +## Global Constraints + +- 仅在 `dailywork_report/pathsmoothing_rep/` 下两个指定 HTML 路径重建内容;它们在本任务基线提交中尚未跟踪,因此 Git 可将首次纳入版本控制的重建页面显示为新增文件。不修改路径平滑算法、测试、地图、粗路径或两份 Markdown 报告的事实内容。 +- 不使用任何外部资源:不使用 CDN、网络请求、外部图片、外部字体、第三方库或构建步骤。 +- 算法页必须具备六个可访问步骤:粗路径、局部窗口、五次 Hermite、局部替换、曲率—弧长、安全与质量门。 +- 问题页必须具备三个可访问问题场景;每个场景同时可见“实际发生”“正确应有”“差异原因”。 +- 所有典型坐标、曲率图形和车辆示意均必须明确标注为机制解释,不得暗示为项目运行时实测结果。 +- `RectangleDetour` 只陈述已复现的 `Success → InvalidInput`;失败点仍标记“待定位”,不得画出伪造的具体坏样本或根因。 +- 用户已授权整体删除并从头重建两份旧 HTML;工作区存在无关改动,每个任务的提交只能包含其对应的一份 HTML,不得带入其他文件。 + +## File Structure + +| 文件 | 职责 | +|---|---| +| `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` | 六步 Local G2 路径几何演示、Hermite 曲线生成、键盘/按钮步骤导航与安全质量门示意。 | +| `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` | 三个问题的实际/正确对照、场景切换与证据边界标注。 | + +--- + +### Task 1: 重做算法页为六步路径几何演示 + +**Files:** +- Modify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` + +**Interfaces:** +- Consumes: 无运行时数据;仅使用固定、标为典型示例的二维点和已确认的 G2 术语/门限。 +- Produces: `setStep(index)`、`buildQuinticPath()`、`stepButtons`、`#algorithm-diagram`、`#step-title`、`#step-description`、`#step-status`、`#prev-step` 与 `#next-step`,供 HTML 初次渲染、按钮和左右方向键共用。 + +- [ ] **Step 1: 先运行会失败的结构验证** + +Run: + +```powershell +$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html' +$html = Get-Content -Raw -Encoding UTF8 $file +$required = @('id="algorithm-diagram"', 'data-step="0"', 'data-step="5"', 'id="prev-step"', 'id="next-step"', 'function buildQuinticPath', 'function setStep') +$missing = @($required | Where-Object { -not $html.Contains($_) }) +if ($missing.Count -gt 0) { throw "Algorithm visual contract missing: $($missing -join ', ')" } +``` + +Expected: FAIL because the current static flow page has no interactive step contract. + +- [ ] **Step 2: 整体删除旧卡片页面并从头重建,让主 SVG 成为视觉中心** + +Use `apply_patch` to整体删除当前文件内容并添加一个响应式新文档,结构如下: + +```html +
+
…候选层已实现、正式发布流水线尚未接入…
+

典型示例:用于解释机制,不代表某次测试的精确坐标。

+ +
+

+ + Local G2 五次 Hermite 局部路径平滑步骤 + 典型粗路径在局部曲率跳变处被五次 Hermite 曲线安全替换的六步示意。 + + +
+
+
+``` + +CSS requirements: + +- `svg { width: 100%; height: auto; }`,不再使用 `min-width` 和横向滚动容器;窄屏按 `viewBox` 等比缩放。 +- 用同一组语义颜色稳定表达:灰色原始粗路径、蓝色窗口/约束、绿色已接受候选、琥珀色发布边界、红色拒绝或阻断;同时配合实线/虚线、文字和符号。 +- `.scene-layer` 默认淡出,`[data-step="N"] .scene-N` 显示;`prefers-reduced-motion: reduce` 时禁用转场。 +- 不设置固定视口高度、不设置内部滚动,并保证按钮触摸目标和焦点状态清晰。 + +- [ ] **Step 3: 用真实五次 Hermite 基函数绘制典型候选曲线** + +In the page script, define the fixed example endpoints and use the six quintic Hermite basis functions—not an SVG cubic Bézier substitute—to sample the visual candidate: + +```javascript +const hermite = { + p0: { x: 290, y: 462 }, p1: { x: 690, y: 258 }, + d0: { x: 180, y: 0 }, d1: { x: 210, y: -135 }, + a0: { x: 0, y: -18 }, a1: { x: 22, y: -12 } +}; + +function quinticBasis(t) { + const t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t; + return [ + 1 - 10 * t3 + 15 * t4 - 6 * t5, + t - 6 * t3 + 8 * t4 - 3 * t5, + 0.5 * (t2 - 3 * t3 + 3 * t4 - t5), + 10 * t3 - 15 * t4 + 6 * t5, + -4 * t3 + 7 * t4 - 3 * t5, + 0.5 * (t3 - 2 * t4 + t5) + ]; +} + +function buildQuinticPath() { + const points = []; + for (let i = 0; i <= 48; i += 1) { + const [h00, h10, h20, h01, h11, h21] = quinticBasis(i / 48); + points.push({ + x: h00 * hermite.p0.x + h10 * hermite.d0.x + h20 * hermite.a0.x + h01 * hermite.p1.x + h11 * hermite.d1.x + h21 * hermite.a1.x, + y: h00 * hermite.p0.y + h10 * hermite.d0.y + h20 * hermite.a0.y + h01 * hermite.p1.y + h11 * hermite.d1.y + h21 * hermite.a1.y + }); + } + return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' '); +} +``` + +Set the generated string on `#quintic-candidate`. Draw a separate raw polyline that shares the same window endpoints but has a visible heading/curvature break in its interior. Add persistent labels for start, end, direction, window boundary and `κ` jump; do not attach real-world units or claim these fixed coordinates are measured data. + +- [ ] **Step 4: 实现六个可读状态的 SVG 图层** + +Create six SVG groups, each carrying both `scene-layer` and `scene-0` through `scene-5` as appropriate. They must communicate these exact visual effects: + +```text +scene-0: 原始离散点、方向箭头、突变点,候选曲线隐藏。 +scene-1: 左右窗口边界和淡蓝色局部带高亮,其余粗路径降低不透明度。 +scene-2: 两端切向箭头、二阶趋势弧线、虚线 quintic-candidate 可见。 +scene-3: 灰色原折线与绿色候选曲线叠加,接缝用“替换开始/结束”标记。 +scene-4: 上方替换后路径;下方 κ—s 趋势示意显示原始跳变与候选连续过渡,并标“趋势示意,非实测数据”。 +scene-5: 三个车辆轮廓沿候选曲线放置;净空带、通过标记和“碰撞 / 净空 / 曲率 / 偏移”四个质量门可见。 +``` + +Use ``, ``, ``, ``, `` and simple `` vehicle rectangles; do not use raster images. Put the existing “正式发布流水线尚未接入”的事实边界 below the visual, outside the six state layers. + +- [ ] **Step 5: 接入状态更新与键盘操作** + +Use one state function and no inline event handlers: + +```javascript +const steps = [ + ['0 / 5', '原始 Hybrid A* 粗路径', '可行离散路径在局部接口处仍可能有曲率跳变。'], + ['1 / 5', '检测并框定局部窗口', '只处理跳变附近,不重新搜索整条路径。'], + ['2 / 5', '由端点约束构造五次 Hermite 候选', '位置、切向和曲率趋势共同确定局部曲线。'], + ['3 / 5', '替换窗口内部的原始几何', '窗口外路径保持不变,接缝需要连续。'], + ['4 / 5', '观察曲率—弧长连续性', '目标是消除接口处的趋势跳变,而不是只让外形更圆。'], + ['5 / 5', '经安全与质量门决定接受或回退', '候选必须同时满足车体安全、净空、曲率和偏移约束。'] +]; + +function setStep(index) { + const next = Math.max(0, Math.min(steps.length - 1, index)); + const [status, title, description] = steps[next]; + document.getElementById('local-g2-algorithm-demo').dataset.step = String(next); + document.getElementById('step-status').textContent = `步骤 ${status}`; + document.getElementById('step-title').textContent = title; + document.getElementById('step-description').textContent = description; + stepButtons.forEach((button) => button.setAttribute('aria-pressed', String(Number(button.dataset.step) === next))); + document.getElementById('prev-step').disabled = next === 0; + document.getElementById('next-step').disabled = next === steps.length - 1; +} +``` + +Declare `stepButtons` before `setStep`, register each button, register `#prev-step`/`#next-step`, handle only unmodified `ArrowLeft` and `ArrowRight` key presses, then call `setStep(0)`. Do not override keyboard interaction when the event target is a form control. + +- [ ] **Step 6: 重新运行算法页验证,确认由失败转为通过** + +Run the Step 1 command again, then run: + +```powershell +$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html' +$html = Get-Content -Raw -Encoding UTF8 $file +$terms = @('典型示例:用于解释机制', '曲率—弧长', '趋势示意,非实测数据', '完整车体安全', '正式发布流水线尚未接入') +$missing = @($terms | Where-Object { -not $html.Contains($_) }) +if ($missing.Count -gt 0) { throw "Algorithm visual content missing: $($missing -join ', ')" } +Write-Output 'Algorithm interactive visualization checks passed.' +``` + +Expected: `Algorithm interactive visualization checks passed.` + +--- + +### Task 2: 重做问题页为错误与正确预期的几何对照 + +**Files:** +- Modify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` + +**Interfaces:** +- Consumes: 已确认的三类证据、`RectangleDetour` 端到端输出、窗口约束数值和弧长陈旧风险的事实边界。 +- Produces: `selectIssue(issueId)`、`issueButtons`、`#issue-visual`、`#issue-evidence`、`#issue-title`、`#issue-actual`、`#issue-expected`、`#issue-cause`、`#issue-action`、`#prev-issue` 与 `#next-issue`。 + +- [ ] **Step 1: 先运行会失败的结构验证** + +Run: + +```powershell +$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' +$html = Get-Content -Raw -Encoding UTF8 $file +$required = @('id="issue-visual"', 'data-issue="baseline"', 'data-issue="window"', 'data-issue="arclength"', 'function selectIssue', 'id="issue-actual"', 'id="issue-expected"', 'id="issue-cause"') +$missing = @($required | Where-Object { -not $html.Contains($_) }) +if ($missing.Count -gt 0) { throw "Issue visual contract missing: $($missing -join ', ')" } +``` + +Expected: FAIL because the current page has only static issue cards and one causal flow diagram. + +- [ ] **Step 2: 整体删除旧问题卡片页面并构建共享选择器与三栏事实说明** + +Use `apply_patch` to整体删除当前文件内容并添加一个新文档,结构如下: + +```html +
+
…三种证据等级…
+ +
+

+ +
+

实际发生

+

正确应有

+

差异原因

+
+

下一步:

+
+
+
+``` + +Use a single shared scale and distinct, labelled SVG lanes rather than three textual cards. Keep the priority/action order in a compact section below the interactive visual, not above it. + +- [ ] **Step 3: 画出三个“实际 / 正确”对照场景,并保持证据边界** + +Create `.issue-scene` SVG groups and make the selected group visible using `[data-issue="…"]` CSS. Each scene must implement these marks: + +```text +baseline: + - 上方:概念性 RectangleDetour 绕障路径、障碍物、灰色原始路径;红色问号标“首次失败样本待定位”。 + - 中间:实际链路 Hybrid A* Success → 原始基线复验 InvalidInput ⛔ → G2 候选未开始。 + - 下方:正确链路 Hybrid A* Success → 原始基线复验 Success → G2 候选评价。 + - 固定脚注:概念性几何,不代表尚未定位的实际坏样本。 + +window: + - 上方:同一条典型路径上的事件 A/B 和弧长标尺,中心距离直接标为 1.0 m。 + - 中间左侧“实际”:两个 ±0.8 m 影响范围重叠后合并,合并总区间标“> 0.8 m”,红色叉号和“无候选”。 + - 中间右侧“正确”:可行的拆分窗口或一致的长度策略,绿色窗口 A/B 和“可评价候选”。 + - 下方:明确标“静态分析确认的逻辑缺口”。 + +arclength: + - 上方:替换前路径的 A、B 两个局部窗口和原始弧长标尺。 + - 下方左侧“实际风险”:A 替换后路径长度改变,B-old 仍按旧弧长落在偏早位置;用虚线箭头表达旧映射。 + - 下方右侧“正确”:重算/稳定锚点后 B-new 落在预期局部;用实线箭头表达新映射。 + - 固定脚注:待正式多区域流水线接入后通过回归测试验证。 +``` + +Do not draw a red collision marker or concrete bad curvature sample in `baseline`; only the question marker is allowed there. Pair every colored status with text (`实际`, `正确`, `待定位`, `无候选`, `重定位`) and a different line style or marker. + +- [ ] **Step 4: 接入问题状态数据、导航和可访问性** + +In the page script, use immutable descriptive data and one update function: + +```javascript +const issues = { + baseline: { + evidence: '已复现故障 · RectangleDetour', + title: '原始基线在 Local G2 开始前被拒绝', + actual: 'Hybrid A* 粗路径规划成功,但原始基线统一复验返回 InvalidInput;候选生成没有开始。', + expected: '同一条成功规划的粗路径应先作为可行基线通过统一复验,再进入候选评价。', + cause: '首次非法数值或超限曲率样本尚未定位;不能把该失败归因于 G2 候选。', + action: '记录首次异常的方向段、样本、曲率、净空和验证结果,在不放松安全门的前提下定位源头。' + }, + window: { + evidence: '静态分析确认的逻辑缺口', + title: '合并范围比可用候选窗口更宽', + actual: '相距 1.0 m 的事件被 ±0.8 m 范围合并,但候选总长度不能超过 0.8 m,结果没有候选。', + expected: '窗口合并和最大总长度应采用一致语义,或在不满足时拆分为可行局部窗口。', + cause: '影响范围的合并规则与候选总长度约束没有共同的可行性判断。', + action: '加入 1.0 m 间距回归,统一窗口长度语义并验证候选仍可生成。' + }, + arclength: { + evidence: '待验证集成风险', + title: '区域 A 替换后,区域 B 的弧长坐标可能陈旧', + actual: 'A 拼接并重算弧长后,B 若仍使用替换前坐标,可能指向错误局部。', + expected: '处理 B 前应按当前路径重定位,或由稳定锚点映射恢复其原始语义位置。', + cause: '候选记录的原始弧长与每次拼接后重新计算的当前弧长处于不同坐标系。', + action: '构造 A 改变长度、B 仍准确定位的双区域回归,再接入正式流水线。' + } +}; + +function selectIssue(issueId) { + const issue = issues[issueId]; + if (!issue) return; + document.getElementById('local-g2-issue-demo').dataset.issue = issueId; + document.getElementById('issue-evidence').textContent = issue.evidence; + document.getElementById('issue-title').textContent = issue.title; + document.getElementById('issue-actual').textContent = issue.actual; + document.getElementById('issue-expected').textContent = issue.expected; + document.getElementById('issue-cause').textContent = issue.cause; + document.getElementById('issue-action').textContent = issue.action; + issueButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.issue === issueId))); +} +``` + +Declare an ordered `issueIds = ['baseline', 'window', 'arclength']`, implement previous/next by index, register button clicks, then call `selectIssue('baseline')`. Make the initial baseline state useful without JavaScript by placing its copy in the HTML before the script runs, then allow JavaScript to overwrite it with the same factually equivalent text. + +- [ ] **Step 5: 重新运行问题页验证,确认由失败转为通过** + +Run the Step 1 command again, then run: + +```powershell +$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' +$html = Get-Content -Raw -Encoding UTF8 $file +$terms = @('Success → InvalidInput', '首次失败样本待定位', '1.0 m', '±0.8 m', '> 0.8 m', '弧长重算', '实际发生', '正确应有', '差异原因') +$missing = @($terms | Where-Object { -not $html.Contains($_) }) +if ($missing.Count -gt 0) { throw "Issue visual content missing: $($missing -join ', ')" } +Write-Output 'Issue interactive visualization checks passed.' +``` + +Expected: `Issue interactive visualization checks passed.` + +--- + +### Task 3: 离线完整性、交互契约和可视化验收 + +**Files:** +- Verify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` +- Verify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` + +**Interfaces:** +- Consumes: 两页已实现的 DOM id、数据属性、函数名和离线资源限制。 +- Produces: 可复查的 PowerShell 验证输出,以及在浏览器可用时的人工交互验收结论。 + +- [ ] **Step 1: 验证离线性、文件结构和静态交互契约** + +Run: + +```powershell +$files = @( + 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html', + 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' +) +$contracts = @{ + $files[0] = @('', '', 'function buildQuinticPath', 'function setStep', 'id="algorithm-diagram"', 'data-step="5"') + $files[1] = @('', '', 'function selectIssue', 'id="issue-visual"', 'data-issue="arclength"', 'id="issue-cause"') +} +foreach ($file in $files) { + $html = Get-Content -Raw -Encoding UTF8 $file + $missing = @($contracts[$file] | Where-Object { -not $html.Contains($_) }) + if ($missing.Count -gt 0) { throw "$file missing: $($missing -join ', ')" } + if ($html -match 'https?://|]+\bsrc\s*=|]+\bsrc\s*=') { throw "$file must not depend on external resources." } + if (($html -split ' **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Export a factual Local G2 visualization that shows the ordinary strict `Unchanged` result and the recorded clearance-rejected diagnostic candidate without publishing or recommending that candidate. + +**Architecture:** The existing comparison service remains the source for normal Local G2 status and output. A test/demo-only evidence loader reads a compact immutable record of `single-turn/s0/r0/w5/seed2`, reconstructs a visual-only spliced path with the existing preprocessor/splicer/analyzer, and appends it to an immutable figure model. The existing SVG/PNG/CSV exporter then produces the six standard figures plus a seventh diagnostic figure only for that augmented model. + +**Tech Stack:** C# 10 targeting `netstandard2.0`, Newtonsoft.Json 13.0.4 already referenced by `ClumsyPilot.csproj`, existing `System.Drawing` PNG renderer, PowerShell verification scripts, .NET 10 Windows verification host. + +## Global Constraints + +- Do not modify `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/`, evaluator, validator, publication, or recommendation logic. +- The normal comparison must retain the actual Local G2 `PathSmoothingStatus`; a diagnostic candidate must never create a `PathSmoothingResult` or a recommendation. +- Evidence is fixed to fixture SHA-256 `3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563`, batch SHA-256 `ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed`, and stable key `single-turn/s0/r0/w5/seed2`. +- Preserve normal-export file stems `01-coarse-path-overview` through `06-curvature-comparison` and its six-file contract. +- The diagnostic candidate must be labelled `净空拒绝,未发布`; do not render a collision cross because the evidence records a clearance rejection, not an occupied-cell collision. +- Keep trajectories point-only. Do not add SVG paths or dashed stroke rendering. +- Place generated artifacts only below `ClumsyPilot/obj/path_smoothing_reports`. +- Use targeted `git add -- ` and `git commit --only -- `; do not include unrelated worktree changes. + +--- + +## File Structure + +| Path | Responsibility | +| --- | --- | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` | Adds normal Local G2 to the immutable default offline comparison order. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` | Holds normal Local G2 and diagnostic-candidate colors. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` | Adds normal Local G2 series and CSV metric row. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` | Creates an immutable model copy with one added diagnostic series. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` | Moves a five-entry legend upward enough to remain inside the fixed figure height. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` | Names the optional seventh figure. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` | Adds Local G2 to normal comparison figures and conditionally creates figure 07. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json` | Immutable compact evidence extract used by the visual-only route. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs` | Parses and verifies evidence identity, geometry, and rejection state. | +| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` | Reconstructs renderable candidate geometry and invokes the existing exporter. | +| `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` | Verifies and exports the augmented seven-file report. | +| `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` | Checks Local G2 default order and normal comparison semantics. | +| `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` | Checks normal Local G2 figure/CSV content and visual layout contracts. | +| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1` | Checks evidence parsing and deterministic rejection validation. | +| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1` | Calls the verification host for the seven-file diagnostic report. | +| `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1` | Builds and exports the user-facing Local G2 diagnostic image. | + +## Task 1: Add Normal Local G2 To Existing Comparison Reports + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` +- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` + +**Interfaces:** +- Consumes: `PathSmoothingComparisonRequest(PathSmoothingRequest, IReadOnlyList methods = null)` and `SmoothingFigureModelBuilder.Build(PathSmoothingComparisonResult, PlanningGridMap, Pose2D, Pose2D, string, string)`. +- Produces: A default method order of `CubicBSpline`, `LocalCubicBezier`, `PiecewiseQuintic`, `LocalG2Quintic`; figure key `local-g2`; CSV method `LocalG2Quintic`; unchanged standard six-figure export count. + +- [ ] **Step 1: Write the failing default-order and standard-model assertions** + + In `verify_path_smoothing_comparison.ps1`, parse the Local G2 enum and construct a request without the optional methods list. Add these assertions after the explicit three-method request assertions: + + ```powershell + $localG2 = [Enum]::Parse($methodType, 'LocalG2Quintic') + $defaultComparisonRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-SmoothingRequest), $null)) + Assert-Equal 4 $defaultComparisonRequest.Methods.Count 'Default comparison must include Local G2.' + Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' (($defaultComparisonRequest.Methods | ForEach-Object ToString) -join ',') 'Default comparison order must be stable.' + ``` + + In `verify_path_smoothing_svg_csv.ps1`, add `#56B4E9` to the expected normal SVG colors, assert that `$model.Series` includes a series whose `Key` is `local-g2`, and assert that the generated CSV contains `LocalG2Quintic,Unchanged` for the frozen `single-turn` request. Add assertions that the standard figure set still contains only the six existing stems. + + In `Program.cs`, add a `VerifyNormalLocalG2Series(SmoothingFigureModel model)` call immediately after `VerifySixFigureDefinitionContract(model)`. It must require exactly one `local-g2` series and a `PathSmoothingStatus` value defined by the enum. If its strict output path is visible, require that it has at least two samples. Do not hard-code `Unchanged` for this artificial high-curvature host fixture; the frozen `single-turn` evidence assertion in the SVG/CSV test owns that requirement. + +- [ ] **Step 2: Run the focused checks and confirm they fail before implementation** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 + ``` + + Expected: the comparison assertion reports three default methods and the SVG/CSV check cannot find `local-g2` or `#56B4E9`. + +- [ ] **Step 3: Implement the smallest normal-comparison extension** + + Append the enum in the existing default array, preserving all existing order: + + ```csharp + private static readonly SmoothingMethod[] DefaultMethods = + { + SmoothingMethod.CubicBSpline, + SmoothingMethod.LocalCubicBezier, + SmoothingMethod.PiecewiseQuintic, + SmoothingMethod.LocalG2Quintic, + }; + ``` + + Add these fixed colors to `IeeeFigureStyle`: + + ```csharp + public const string LocalG2Color = "#56B4E9"; + public const string LocalG2DiagnosticColor = "#B1373E"; + ``` + + In `SmoothingFigureModelBuilder`, append the normal series and metric row after the existing piecewise-quintic entries. Use the exact stable key and label: + + ```csharp + series.Add(CreateSeries( + Find(comparison, SmoothingMethod.LocalG2Quintic), + SmoothingMethod.LocalG2Quintic, + "local-g2", + "局部 G2", + IeeeFigureStyle.LocalG2Color, + string.Empty, + false, + map)); + + CreateRow(Find(comparison, SmoothingMethod.LocalG2Quintic), "LocalG2Quintic", "局部 G2") + ``` + + In `SmoothingFigureSetBuilder.Build`, resolve `local-g2` with the other standard series and include it in only the existing all-path and curvature comparisons: + + ```csharp + SmoothingFigureSeries localG2 = Find(model, "local-g2"); + // Add View(localG2, 1d) after View(quintic, 1d) in figures 02 and 06. + ``` + + Keep individual figures `03` through `05` unchanged. In `SmoothingFigureDefinition`, use a five-entry-safe legend origin: + + ```csharp + public double LegendYPoints => LegendEntries.Count > 4 ? 332d : 340d; + ``` + + Do not special-case `Unchanged`: `PathSmoothingComparisonService` already supplies its strict path and status. Do not modify that service or the ranker. + +- [ ] **Step 4: Run the focused checks and confirm they pass** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 + ``` + + Expected: all three scripts exit `0`; normal exports retain exactly six figures; the Local G2 row is present with its actual `Unchanged` status. + +- [ ] **Step 5: Commit only Task 1 files** + + ```powershell + git add -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` + ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 ` + ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 ` + ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs + git -c core.autocrlf=false diff --cached --check + git commit --only -m "feat: show Local G2 in smoothing comparisons" -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` + ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 ` + ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 ` + ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs + ``` + +### Task 2: Freeze And Validate The Diagnostic Evidence Extract + +**Files:** +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs` +- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1` + +**Interfaces:** +- Produces: `public sealed class LocalG2DiagnosticEvidenceLoader` with `public LocalG2DiagnosticEvidence LoadAndVerify(string evidencePath)`. +- Produces: `LocalG2DiagnosticEvidence` properties `ScenarioId`, `FixtureSha256`, `CandidateStableKey`, `CandidateSha256`, `CandidateIndex`, `SegmentIndex`, `WindowStartArcLengthMeters`, `WindowEndArcLengthMeters`, start/end curvatures, `CandidatePoints`, `EvaluatorResult`, `StopGate`, and `PublishedStatus`. +- Consumed later by: `LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`. + +- [ ] **Step 1: Write the failing evidence-loader verification script** + + Create `verify_path_smoothing_local_g2_diagnostic_evidence.ps1`. Load `ClumsyPilot.dll`, resolve `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.LocalG2DiagnosticEvidenceLoader`, and invoke `LoadAndVerify` with the new fixture path. Assert all of these exact values: + + ```powershell + Assert-Equal 'single-turn' $evidence.ScenarioId 'Diagnostic evidence scenario must be stable.' + Assert-Equal 'single-turn/s0/r0/w5/seed2' $evidence.CandidateStableKey 'Diagnostic evidence key must be stable.' + Assert-Equal 5 $evidence.CandidateIndex 'Diagnostic evidence candidate index must be stable.' + Assert-Equal 10 $evidence.CandidatePoints.Count 'Diagnostic evidence must retain all ten recorded samples.' + Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evidence must retain the observed evaluator result.' + Assert-Equal 'Clearance' $evidence.StopGate 'Diagnostic evidence must retain the observed stop gate.' + Assert-Equal 'Unchanged' $evidence.PublishedStatus 'Diagnostic evidence must retain the strict final status.' + ``` + + Copy the JSON to a uniquely named temp path, replace only `"StopGate": "Clearance"` with `"StopGate": "Collision"`, and require that `LoadAndVerify` throws. Delete the temp copy in `finally`. + +- [ ] **Step 2: Run the evidence check and confirm it fails before implementation** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 + ``` + + Expected: failure because the loader type and evidence file do not exist. + +- [ ] **Step 3: Add the immutable compact evidence extract and loader** + + Create the JSON file with this exact top-level contract and the ten recorded `CandidatePoints`. Preserve the displayed IEEE-754 decimal values; they are the evidence values, not rounded drawing inputs: + + ```json + { + "SourceMeasurementBatchSha256": "ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed", + "FixtureSha256": "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", + "ScenarioId": "single-turn", + "CandidateStableKey": "single-turn/s0/r0/w5/seed2", + "CandidateSha256": "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392", + "CandidateIndex": 5, + "SegmentIndex": 0, + "WindowStartArcLengthMeters": 1.9199999999999982, + "WindowEndArcLengthMeters": 2.1199999999999983, + "StartGeometricCurvaturePerMeter": 0.41666666666666663, + "EndGeometricCurvaturePerMeter": 0.0, + "StartVehicleCurvaturePerMeter": 0.41666666666666663, + "EndVehicleCurvaturePerMeter": 0.0, + "EvaluatorResult": "InsufficientClearance", + "StopGate": "Clearance", + "PublishedStatus": "Unchanged", + "CandidatePoints": [ + { "X": 2.7216397035494579, "Y": 1.7279184433567971, "ReferenceArcLengthMeters": 1.9199999999999982, "HeadingRadians": 0.799999999999999, "UnwrappedHeadingRadians": 0.799999999999999, "Source": 4 }, + { "X": 2.7355188205174521, "Y": 1.7423187041196302, "ReferenceArcLengthMeters": 1.9399999999999982, "HeadingRadians": 0.80751185024996908, "UnwrappedHeadingRadians": 0.80751185024996908, "Source": 4 }, + { "X": 2.7492871654421882, "Y": 1.7568248978050907, "ReferenceArcLengthMeters": 1.9599999999999982, "HeadingRadians": 0.8157618125921976, "UnwrappedHeadingRadians": 0.8157618125921976, "Source": 4 }, + { "X": 2.7629234692741469, "Y": 1.7714552535786874, "ReferenceArcLengthMeters": 1.9799999999999982, "HeadingRadians": 0.82542852165703573, "UnwrappedHeadingRadians": 0.82542852165703573, "Source": 4 }, + { "X": 2.7764244476704873, "Y": 1.786210614200662, "ReferenceArcLengthMeters": 1.9999999999999982, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 }, + { "X": 2.7925350453990334, "Y": 1.803999635249544, "ReferenceArcLengthMeters": 2.0239999999999982, "HeadingRadians": 0.835253334801853, "UnwrappedHeadingRadians": 0.835253334801853, "Source": 4 }, + { "X": 2.80865418003672, "Y": 1.8217809211927092, "ReferenceArcLengthMeters": 2.0479999999999983, "HeadingRadians": 0.83333333355178374, "UnwrappedHeadingRadians": 0.83333333355178374, "Source": 4 }, + { "X": 2.8248074262445737, "Y": 1.8395312269498194, "ReferenceArcLengthMeters": 2.0719999999999983, "HeadingRadians": 0.8318933325232104, "UnwrappedHeadingRadians": 0.8318933325232104, "Source": 4 }, + { "X": 2.8409691920174533, "Y": 1.8572737784943611, "ReferenceArcLengthMeters": 2.0959999999999983, "HeadingRadians": 0.83237333274470626, "UnwrappedHeadingRadians": 0.83237333274470626, "Source": 4 }, + { "X": 2.8571139169604551, "Y": 1.8750318365841865, "ReferenceArcLengthMeters": 2.1199999999999983, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 } + ] + } + ``` + + Implement the public loader in the test namespace using `Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(evidencePath))`. Use `InvalidDataException` for every rejected input. The validation must require the three exact SHA/key constants, `ScenarioId == "single-turn"`, `CandidateIndex == 5`, `SegmentIndex == 0`, `EvaluatorResult == "InsufficientClearance"`, `StopGate == "Clearance"`, and `PublishedStatus == "Unchanged"`. + + Validate the two window values and all point coordinates/headings/reference arc lengths with this helper: + + ```csharp + private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); + ``` + + Require exactly ten points, first/last reference arcs equal the window endpoints within `1e-8d`, strictly increasing reference arcs, `Source == (int)SmoothedPathPointSource.LocalG2Transition`, and non-null start/end curvature values. Return only after all checks pass. + +- [ ] **Step 4: Run the evidence verification and confirm it passes** + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 + ``` + + Expected: exit `0`; the valid extract loads and the altered stop gate is rejected. + +- [ ] **Step 5: Commit only Task 2 files** + + ```powershell + git add -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs ` + ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 + git -c core.autocrlf=false diff --cached --check + git commit --only -m "test: freeze Local G2 diagnostic evidence" -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs ` + ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 + ``` + +### Task 3: Build The Visual-Only Candidate And Optional Figure 07 + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` +- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` +- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1` + +**Interfaces:** +- Produces: `internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series)` that preserves all existing model layout, metric rows, endpoints, and scales. +- Produces: `SmoothingFigureKind.LocalG2DiagnosticCandidate` and optional stem `07-local-g2-diagnostic-candidate`. +- Produces: `public SmoothingReportExportResult LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`. +- Produces: a host verification command that accepts a fixture path and evidence path, plus an export command that accepts fixture, evidence, and output-directory paths. + +- [ ] **Step 1: Write the failing seven-file verification host branch and wrapper script** + + In `Program.Main`, add command dispatch before the existing fixture/export cases: + + ```csharp + if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic") + { + VerifyLocalG2Diagnostic(arguments[1], arguments[2]); + Console.WriteLine("Local G2 diagnostic visualization verification completed."); + return 0; + } + if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic") + { + ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]); + return 0; + } + ``` + + Create the wrapper script to resolve the fixture and evidence paths and run this command: + + ```powershell + & dotnet run --project $hostProject --no-restore -- --verify-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + ``` + + Run it before the demo/model implementation. Expected: build error because `VerifyLocalG2Diagnostic` and `ExportLocalG2Diagnostic` do not yet exist. + +- [ ] **Step 2: Add immutable model augmentation and the optional figure definition** + + Add this internal method to `SmoothingFigureModel`: + + ```csharp + internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series) + { + if (series == null) throw new ArgumentNullException(nameof(series)); + var combined = new List(Series.Count + 1); + for (int index = 0; index < Series.Count; index++) + { + if (Series[index].Key == series.Key) + throw new ArgumentException("Figure series keys must be unique.", nameof(series)); + combined.Add(Series[index]); + } + combined.Add(series); + var copy = new SmoothingFigureModel( + ScenarioId, ScenarioLabel, WorldXMinMeters, WorldXMaxMeters, WorldYMinMeters, WorldYMaxMeters, + PathPanelX, PathPanelY, PathPanelWidth, PathPanelHeight, + CurvaturePanelX, CurvaturePanelY, CurvaturePanelWidth, CurvaturePanelHeight, + MetricsPanelX, MetricsPanelY, MetricsPanelWidth, MetricsPanelHeight, + Obstacles, combined, MetricRows, Start, Goal) + { + PathScaleX = PathScaleX, + PathScaleY = PathScaleY, + }; + return copy; + } + ``` + + Add `LocalG2DiagnosticCandidate` to `SmoothingFigureKind`. In `SmoothingFigureSetBuilder.Build`, retain the six normal definitions first. Use a non-throwing `TryFind` helper for key `local-g2-diagnostic`; when it succeeds, append exactly this overhead figure: + + ```csharp + BuildOverhead( + SmoothingFigureKind.LocalG2DiagnosticCandidate, + "07-local-g2-diagnostic-candidate", + "G2 诊断候选:净空拒绝,未发布;严格输出=原始路径", + model, + true, + View(raw, 0.45d), + View(diagnostic, 1d)) + ``` + + The diagnostic series must carry key `local-g2-diagnostic`, status `PathSmoothingStatus.Infeasible`, color `IeeeFigureStyle.LocalG2DiagnosticColor`, and an empty `ViolationMarkers` list. Do not reuse `SmoothingFigureModelBuilder.CreateSeries`, because its generic `Infeasible` behavior synthesizes a violation cross when it cannot identify an occupied point. + +- [ ] **Step 3: Implement `LocalG2DiagnosticVisualizationDemo` with the existing geometry components** + + The class stays in namespace `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test` and creates no `PathSmoothingResult`. Its `Export` method must execute this exact sequence: + + ```csharp + LocalG2DiagnosticEvidence evidence = _evidenceLoader.LoadAndVerify(evidencePath); + PathSmoothingComparisonRequest request = FindFixtureRequest(fixturePath, evidence.ScenarioId); + PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken); + PathSmoothingComparisonEntry localG2 = FindEntry(comparison, SmoothingMethod.LocalG2Quintic); + Require(localG2 != null && localG2.Status == PathSmoothingStatus.Unchanged, + "Strict Local G2 result must be Unchanged for the diagnostic evidence."); + RequireSameGeometry(comparison.RawPathBaseline.Path, localG2.Path); + _preprocessor.TryPrepare(request.SmoothingRequest, out PreparedPath prepared, out string reason); + LocalG2CandidateGeometry candidate = CreateCandidate(evidence); + _splicer.TryReplace(prepared, candidate, out PreparedPath spliced, out reason); + _analyzer.TryAnalyze(spliced.Segments, request.SmoothingRequest.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason); + CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; + CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; + SmoothingFigureModel normal = _figureBuilder.Build( + comparison, + request.SmoothingRequest.Map, + new Pose2D(first.X, first.Y, first.Heading), + new Pose2D(last.X, last.Y, last.Heading), + evidence.ScenarioId, + evidence.ScenarioId); + SmoothingFigureModel augmented = normal.WithAdditionalSeries(CreateDiagnosticSeries(analysis.Path)); + return _exporter.Export(new SmoothingReportExportRequest { Model = augmented, OutputDirectory = outputDirectory, FileStem = "comparison" }); + ``` + + `FindFixtureRequest` must call `SmoothingScenarioFactory.CreateFixtureRequests(fixturePath)`, locate exactly one request by the matching fixture index from `SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath)`, and reject missing or duplicate `single-turn` IDs. `CreateCandidate` must turn every evidence point into: + + ```csharp + new SmoothingPoint2D( + point.X, point.Y, point.ReferenceArcLengthMeters, + point.HeadingRadians, point.UnwrappedHeadingRadians, + 0d, false, SmoothedPathPointSource.LocalG2Transition) + ``` + + Construct `LocalG2CandidateGeometry` with evidence index/window/curvatures, `0d` left and right lengths, the converted point list, and `true` for `internalConnectionsAreG2`. The zero clearance exists only to satisfy geometry-analysis input validity; it must not be fed to a validator or a metric row. + + `CreateDiagnosticSeries` must convert `analysis.Path` to `SmoothingFigurePoint` values, use label `G2 候选(净空拒绝,未发布)`, and supply `Array.Empty()` as violation markers. Its path can be visualized but is not a published/safe path. + +- [ ] **Step 4: Implement host verification and run it** + + `VerifyLocalG2Diagnostic` must create a fresh temp output directory, invoke the demo, and require all of the following before deleting the directory in `finally`: + + ```csharp + Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); + Require(report.SvgPaths.Count == 7 && report.PngPaths.Count == 7 && File.Exists(report.CsvPath), "Diagnostic export must publish seven SVGs, seven PNGs and one CSV."); + Require(Path.GetFileName(report.SvgPaths[6]) == "07-local-g2-diagnostic-candidate.svg", "Diagnostic SVG stem is incorrect."); + Require(Path.GetFileName(report.PngPaths[6]) == "07-local-g2-diagnostic-candidate.png", "Diagnostic PNG stem is incorrect."); + string diagnosticSvg = File.ReadAllText(report.SvgPaths[6]); + Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), "Diagnostic SVG must contain raw and diagnostic samples."); + Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布"), "Diagnostic SVG must disclose rejection and publication state."); + Require(!diagnosticSvg.Contains("violation-cross"), "Clearance rejection must not be drawn as an obstacle collision."); + Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), "CSV must retain the normal strict Local G2 row."); + ``` + + Run `VerifyPng(File.ReadAllBytes(path))` for every returned PNG and use the existing `ContainsTemporaryFiles` helper to assert atomic publication. Also copy the evidence file to a temp file, alter the fixture hash, call the demo with a separate empty output path, require an exception, and require that the output directory was never created. + + Run: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1 + ``` + + Expected: exit `0`, seven valid figures, raw and diagnostic series in `07`, no fabricated collision cross, and bad evidence rejected atomically. + +- [ ] **Step 5: Commit only Task 3 files** + + ```powershell + git add -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs ` + ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs ` + ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 + git -c core.autocrlf=false diff --cached --check + git commit --only -m "feat: export Local G2 diagnostic candidate" -- ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs ` + ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs ` + ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs ` + ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1 + ``` + +### Task 4: Add The User-Facing Export Script And Perform Full Acceptance + +**Files:** +- Create: `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1` + +**Interfaces:** +- Consumes: the diagnostic host export command, its fixture path, its evidence path, and its bounded output directory. +- Produces: `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png` and six companion standard figures. + +- [ ] **Step 1: Write the failing runner assertion in the PNG smoke test** + + In `verify_path_smoothing_png.ps1`, add a call to the not-yet-created runner with an explicit output directory below `obj/path_smoothing_reports`, followed by an existence assertion for its primary PNG. Its default runner paths must be: + + ```powershell + [string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'), + [string]$EvidencePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-single-turn') + ``` + + Add this smoke-test block after the existing host invocation, changing no unrelated test behavior: + + ```powershell + $runnerPath = Join-Path $PSScriptRoot 'run_local_g2_diagnostic_visualization.ps1' + $runnerOutput = Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-png-smoke' + & powershell -ExecutionPolicy Bypass -File $runnerPath -OutputDirectory $runnerOutput + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $primaryPng = Join-Path $runnerOutput '07-local-g2-diagnostic-candidate.png' + if (-not (Test-Path -LiteralPath $primaryPng)) { + throw "Local G2 diagnostic runner did not publish $primaryPng" + } + ``` + + Run the PNG smoke test. Expected: PowerShell reports that `run_local_g2_diagnostic_visualization.ps1` does not exist, so the runner acceptance assertion fails before implementation. + +- [ ] **Step 2: Implement the bounded export script and extend the PNG smoke test** + + Follow the existing `run_path_smoothing_comparison.ps1` root validation exactly: resolve `$clumsyPilotRoot`, require `$OutputDirectory` to equal or be below `$clumsyPilotRoot\obj\path_smoothing_reports`, resolve both input files, build `ClumsyPilot.csproj`, then execute: + + ```powershell + & dotnet run --project $hostProject --no-restore -- --export-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath $resolvedOutputDirectory + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Output "Local G2 diagnostic visualization written below $resolvedOutputDirectory" + ``` + + In `verify_path_smoothing_png.ps1`, call the new diagnostic verification wrapper after the existing host verification so both standard six-file and diagnostic seven-file image contracts run in the normal PNG check. Retain the runner call and primary-PNG existence assertion added in Step 1; use a `local-g2-png-smoke` output subdirectory below the allowed report root. + +- [ ] **Step 3: Run the full automated acceptance sequence** + + Run these commands in order and inspect every exit code: + + ```powershell + dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1 + powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\run_local_g2_diagnostic_visualization.ps1 + ``` + + Expected: every command exits `0`; the primary PNG, matching SVG, and `comparison.csv` exist under `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn`. + +- [ ] **Step 4: Inspect the generated image and report the factual result** + + Open the primary file with the local image viewer: + + ```text + ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png + ``` + + Confirm visually that the map is nonblank, the gray raw/final path and red diagnostic candidate are both visible, the legend/title disclose `净空拒绝` and `未发布`, there is no collision cross, and text remains inside the 4296-by-3120 PNG frame. Also open `02-all-paths-comparison.png` and `06-curvature-comparison.png` from the same directory to confirm that normal Local G2 appears with `Unchanged` alongside the existing methods. + +- [ ] **Step 5: Commit only Task 4 source/test files** + + Do not commit generated `obj` artifacts. Commit only the runner and PNG smoke-test changes: + + ```powershell + git add -- ` + ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 ` + ClumsyPilot/tests/verify_path_smoothing_png.ps1 + git -c core.autocrlf=false diff --cached --check + git commit --only -m "test: add Local G2 diagnostic visualization runner" -- ` + ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 ` + ClumsyPilot/tests/verify_path_smoothing_png.ps1 + ``` + +## Final Verification Checklist + +- [ ] Re-read [`2026-08-02-local-g2-diagnostic-visualization-design.md`](../specs/2026-08-02-local-g2-diagnostic-visualization-design.md) and map every acceptance criterion to a passing command or visual check above. +- [ ] Run `git -c core.autocrlf=false diff --check` only on the files changed by these tasks. +- [ ] Verify that no `PathSmoothing/LocalG2/` implementation, evaluator, validator, publishing, or ranking file changed. +- [ ] Verify each task commit contains only its listed paths. +- [ ] Report the primary image path and state plainly that the red curve is a rejected diagnostic candidate, while the strict Local G2 result remains `Unchanged`. diff --git a/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md b/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md new file mode 100644 index 0000000..f300c8a --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-daily-summary-job-domain-visualization-upgrade.md @@ -0,0 +1,915 @@ +# Daily Summary Job Domain Visualization Upgrade Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` (recommended) or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Upgrade `daily-summary-job` so it understands the current task, renders the function or algorithm's real domain effect, and connects visible objects to the current problem, cause, consequence, correction, expected result, and task-matched verification evidence. + +**Architecture:** Extend the normalized report facts with optional algorithm views and task-specific validation facts. Keep one diagnostic interaction shell, but render its central canvas through a declarative adapter registry selected from the current task's semantics. Split maintainable CSS and JavaScript assets during skill development, then inline them into the final self-contained HTML during rendering. + +**Tech Stack:** Python 3.12 standard library, `unittest`, HTML5, CSS, vanilla JavaScript, SVG/DOM, Node syntax checks. + +## Global Constraints + +- Implement the approved design in `docs/superpowers/specs/2026-08-03-daily-summary-job-domain-visualization-upgrade-design.md`. +- Modify the personal skill at `C:\Users\admin\.codex\skills\daily-summary-job`; request filesystem approval when the execution environment requires it. +- Do not hard-code trajectory planning as the meaning of algorithm visualization. Select the view from the current task's purpose, observable business objects, inputs, outputs, and correctness constraints. +- A generic flow diagram may assist navigation, but it must not replace a domain effect view when spatial, numeric, temporal, state, search, or structured-data evidence exists. +- Distinguish actual observation, static reconstruction, conceptual preview, verified result, and conflicting evidence in both data and presentation. +- Build validation scenarios from the current task and its correctness constraints. Do not substitute an unrelated fixed test matrix. +- Preserve reports that omit the new optional fields. +- Final HTML must contain no external resource or network dependency. +- Do not modify business code, run expensive tests by default, stage changes, or create Git commits. +- Preserve the current UTF-8, date/module classification, checkpoint limits, stable issue IDs, and Markdown/HTML pairing behavior. + +## User-Approved Scope Adjustment + +This implementation ships only the generic declarative `composite-scene` renderer and the unified diagnosis shell. Do not implement dedicated `spatial-scene`, `cartesian-series`, `graph-network`, `state-machine`, or `data-flow` renderers in this round. Keep the adapter registry as an extension point, so future task-specific work can add those renderers without changing the report contract. + +--- + +## File Structure + +**Modify** + +- `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` — task understanding, visualization-brief generation, domain-view selection, and task-matched validation workflow. +- `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md` — optional algorithm-view, issue-target, solution-preview, verified-result, and task-validation contracts. +- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py` — validate new facts, render the Markdown algorithm section, bundle assets, and validate rendered links. +- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` — schema, backward compatibility, Markdown, asset bundling, adapter, interaction, and CLI regression tests. +- `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html` — diagnostic-shell markup and asset placeholders. +- `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` — UI description and default prompt for task-matched domain visualization. + +**Create** + +- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css` — domain canvas, view modes, diagnostic drawer, evidence states, responsiveness, and reduced-motion styles. +- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js` — declarative scene-object renderer registry and built-in layout strategies. +- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js` — report state, selectors, view switching, target highlighting, diagnosis rendering, and keyboard interaction. + +The three development assets are embedded into every rendered report. They must never remain as runtime `` or ` + + +``` + +- [ ] **Step 4: Implement the asset loader and renderer replacement** + +```python +VISUAL_ASSET_FILES = { + "__VISUAL_STYLES__": "visualization-styles.css", + "__VISUAL_ADAPTERS__": "visualization-adapters.js", + "__VISUAL_RUNTIME__": "visualization-runtime.js", +} + + +def load_visual_assets(asset_dir: Path | None = None) -> dict[str, str]: + root = Path(asset_dir or Path(__file__).parent.parent / "assets") + return { + placeholder: (root / filename).read_text(encoding="utf-8") + for placeholder, filename in VISUAL_ASSET_FILES.items() + } + + +def render_html( + data: dict[str, Any], + template: str, + visual_assets: dict[str, str] | None = None, +) -> str: + validate_report_data(data) + replacements = { + "__REPORT_DATA__": safe_json_for_html(data), + **(visual_assets or load_visual_assets()), + } + rendered = template + for placeholder, value in replacements.items(): + if rendered.count(placeholder) != 1: + label = "report data placeholder" if placeholder == "__REPORT_DATA__" else "visual asset placeholder" + raise ValueError(f"template must contain exactly one {label}: {placeholder}") + rendered = rendered.replace(placeholder, value) + return rendered +``` + +When `--template` points to a custom template, continue loading the trusted bundled assets from the skill's `assets` directory unless a future explicit CLI option changes that contract. + +- [ ] **Step 5: Run bundling tests, full tests, and syntax checks** + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js +``` + +Expected: Python suite passes; both Node checks exit 0. + +--- + +### Task 4: Implement the generic declarative domain-effect renderer + +**Files:** + +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` + +**Interfaces:** + +- Consumes: `algorithm_views[].scene.objects`, `scene.layers`, and the selected view mode. +- Produces: SVG/DOM elements carrying `data-target-id`, `data-evidence-state`, and accessible labels. +- Public JS API: `DailySummaryVisuals.register(name, renderer)`, `.select(name)`, and `.render(view, root, context)`. + +- [ ] **Step 1: Add task-derived adapter assertions** + +Use `visualization_data()` as the business fixture. Add static and generated-HTML assertions: + +```python +def test_domain_adapter_renders_task_objects_not_fixed_demo_content(self): + html = self.require_html_target().render_html( + self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8") + ) + for value in ("local-g2-smoother", "current-path", "curvature-peak", "preview-path"): + self.assertIn(value, html) + self.assertNotIn("固定轨迹示例", html) + +def test_unknown_safe_adapter_has_composite_fallback(self): + data = self.visualization_data() + data["algorithm_views"][0]["adapter"] = "custom-business-domain" + html = self.require_html_target().render_html( + data, TEMPLATE_PATH.read_text(encoding="utf-8") + ) + self.assertIn("custom-business-domain", html) + self.assertIn("composite-scene", html) +``` + +- [ ] **Step 2: Run the focused tests and confirm RED** + +Expected: the fallback or declarative object hooks are missing. + +- [ ] **Step 3: Add safe DOM/SVG construction helpers** + +Implement helpers that assign text through `textContent` and SVG attributes through `setAttribute`; never concatenate untrusted labels into `innerHTML`: + +```javascript +const SVG_NS = 'http://www.w3.org/2000/svg'; + +function element(name, attrs = {}, text = '') { + const node = document.createElement(name); + Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value))); + if (text) node.textContent = text; + return node; +} + +function svgElement(name, attrs = {}) { + const node = document.createElementNS(SVG_NS, name); + Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value))); + return node; +} + +function markTarget(node, object) { + node.dataset.targetId = object.id; + node.dataset.evidenceState = object.evidence_state; + node.setAttribute('tabindex', '0'); + node.setAttribute('role', 'button'); + node.setAttribute('aria-label', `${object.label},${object.evidence_state}`); + return node; +} +``` + +- [ ] **Step 4: Implement adapter strategies over shared primitives** + +Register these layout strategies, while keeping their data task-driven: + +- `composite-scene`: render supplied points, polylines, curves, regions, nodes, edges, state blocks, data items, annotations, and clear unsupported-kind cards for the remainder. + +The generic renderer must filter visible objects from the selected `scene.layers[].object_ids`; it must not invent domain samples. Unknown adapter names must select `composite-scene`. Do not add dedicated renderer implementations in this task. + +- [ ] **Step 5: Add evidence-state and target CSS** + +In `visualization-styles.css`, use line style, icon/text, and color together: + +```css +[data-evidence-state="actual"] { --state-color: var(--blue); } +[data-evidence-state="static"] { --state-color: var(--amber); } +[data-evidence-state="conceptual"] { --state-color: var(--amber); stroke-dasharray: 8 6; opacity: .82; } +[data-evidence-state="verified"] { --state-color: var(--green); } +[data-evidence-state="conflict"] { --state-color: var(--red); stroke-dasharray: 3 4; } +[data-target-id].is-highlighted { filter: drop-shadow(0 0 5px var(--state-color)); } +[data-target-id]:focus-visible { outline: 3px solid #e7a628; outline-offset: 3px; } +``` + +- [ ] **Step 6: Run Python tests and Node syntax checks** + +Use the commands from Task 3 Step 5. Expected: all pass, and no test claims business correctness beyond the current fixture's actual evidence. + +--- + +### Task 5: Build the four-mode interactive diagnosis shell + +**Files:** + +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` + +**Interfaces:** + +- State: `{ algorithmId, issueId, mode, solutionIndex }`. +- Modes: `baseline`, `current`, `proposed`, `verified`. +- Consumes: Task 1 issue links and Task 4 adapter API. + +- [ ] **Step 1: Write failing interaction-hook tests** + +```python +def test_renders_algorithm_selector_four_modes_domain_canvas_and_diagnosis_card(self): + html = self.require_html_target().render_html( + self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8") + ) + for hook in ( + 'id="algorithm-selector"', + 'data-view-mode="baseline"', + 'data-view-mode="current"', + 'data-view-mode="proposed"', + 'data-view-mode="verified"', + 'id="domain-canvas"', + 'id="diagnosis-current"', + 'id="diagnosis-cause"', + 'id="diagnosis-impact"', + 'id="diagnosis-solution"', + 'id="diagnosis-expected"', + 'id="task-validation"', + ): + self.assertIn(hook, html) + +def test_verified_mode_is_guarded_by_verified_result(self): + runtime = (TEMPLATE_PATH.parent / "visualization-runtime.js").read_text(encoding="utf-8") + self.assertIn("hasVerifiedResult", runtime) + self.assertIn("button.disabled", runtime) + self.assertIn("尚无修正后的匹配验证证据", runtime) +``` + +- [ ] **Step 2: Run the focused tests and confirm RED** + +Expected: the new shell hooks and verified-result guard are absent. + +- [ ] **Step 3: Replace the two-column issue workbench with the approved shell** + +Add: + +- algorithm and issue selectors; +- four mode buttons with `aria-pressed`; +- layer toggles; +- central `#domain-canvas`; +- algorithm-stage navigation; +- object diagnosis card; +- expandable source/test evidence; +- existing solution steps, validation gates, and roadmap below the canvas. + +When `algorithm_views` is absent, hide the algorithm controls and retain the legacy text diagnosis behavior. + +- [ ] **Step 4: Implement one state-driven render path** + +In `visualization-runtime.js`, use one render function so selectors, modes, canvas, diagnosis, validation, and buttons never drift: + +```javascript +const report = JSON.parse(document.getElementById('report-data').textContent); +const views = Array.isArray(report.algorithm_views) ? report.algorithm_views : []; +const issues = Array.isArray(report.issues) ? report.issues : []; +const state = { + algorithmId: views[0]?.id || '', + issueId: issues[0]?.id || '', + mode: views.length ? 'baseline' : 'current', + solutionIndex: 0, +}; + +function currentView() { + return views.find((view) => view.id === state.algorithmId) || null; +} + +function currentIssue() { + return issues.find((issue) => issue.id === state.issueId) || null; +} + +function hasVerifiedResult(issue) { + return Boolean(issue?.verified_result?.evidence_state === 'verified' && issue.verified_result.validation_refs?.length); +} + +function renderApp() { + const view = currentView(); + const issue = currentIssue(); + renderSelectors(view, issue); + renderModeButtons(issue); + renderDomainCanvas(view, issue); + renderDiagnosis(issue); + renderTaskValidation(report.task_validation); + renderExistingReportSections(issue); +} +``` + +- [ ] **Step 5: Implement mode-to-layer and diagnosis behavior** + +- `baseline`: show the normal algorithm layer and purpose/input/output/constraints. +- `current`: show current layer, highlight `target_ids`, then `effect_target_ids` in propagation order. +- `proposed`: show `solution_preview.target_ids`, expected result, and conceptual/static label. +- `verified`: enable only when `hasVerifiedResult(issue)`; show verified targets and validation references. + +Clicking or pressing Enter/Space on a visual target must select the linked issue. Arrow keys change issues only when focus is not in a form control; mode buttons and targets retain visible focus. + +- [ ] **Step 6: Add responsive and reduced-motion behavior** + +At desktop width use mode rail + canvas + diagnosis drawer. Under 900px stack the drawer below the canvas. Under 560px use single-column selectors and controls. When `prefers-reduced-motion: reduce` is active, reveal the complete impact path immediately rather than animating it. + +- [ ] **Step 7: Extend rendered-pair validation to cover algorithm facts** + +Update `_validate_rendered_text` so every algorithm view ID, view evidence state, linked issue target ID, correctness criterion ID, and executed check ID exists in the generated HTML. Require the view name, purpose, criterion statement, and check result in Markdown. Keep the existing issue ID/evidence checks and external-resource rejection. + +Add a negative test that removes `curvature-peak` from rendered HTML and expects `validate` to fail with `HTML is missing visual target: curvature-peak`. + +- [ ] **Step 8: Run the full suite and generated-HTML validation** + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js +``` + +Expected: all tests and syntax checks pass; generated HTML contains no external resource. + +--- + +### Task 6: Teach the skill the task-understanding and dynamic-validation workflow + +**Files:** + +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` + +**Interfaces:** + +- Consumes: data contract and renderer from Tasks 1-5. +- Produces: repeatable Agent instructions that create task-matched views and validation facts without requiring the user to fill JSON manually. + +- [ ] **Step 1: Add the mandatory understanding sequence to SKILL.md** + +Insert a concise workflow before report JSON construction: + +```markdown +## Build a task-matched algorithm view + +When today's work changes, diagnoses, or discusses a function or algorithm: + +1. Identify its business purpose, inputs, outputs, stages, observable objects, and correctness constraints from current evidence. +2. Decide what domain effect lets a reader see the algorithm working. Prefer spatial scenes, numeric plots, search/state structures, timelines, or transformed data over a generic flowchart when the evidence supports them. +3. Build one `algorithm_views` entry from actual run/test data when available. Label source reconstruction as `static` and solution prediction as `conceptual`. +4. Link every visual issue to existing stage/object IDs. Show current targets, effect propagation, candidate changes, and verified results as separate states. +5. If the evidence cannot support a credible domain view, list the missing evidence and omit the invented scene. +``` + +- [ ] **Step 2: Replace generic validation wording with task-matched validation** + +```markdown +## Match validation to the current task + +Derive correctness criteria from the selected function or algorithm, then locate only tests, commands, samples, and runtime evidence that directly evaluate those criteria. Record checks actually run, their exact results, and missing evidence separately. If no matching test exists, propose a task-specific check and keep the conclusion unverified. Never claim coverage from an unrelated fixed scenario. +``` + +Retain the existing safety rule against expensive tests by default. + +- [ ] **Step 3: Document the complete schema and one non-prescriptive example** + +In `references/report-schema.md`, document every Task 1 field, allowed evidence states, object/layer linking rules, task-validation shape, verified-result requirements, and legacy behavior. Use one example only to illustrate the contract, and state explicitly that its domain does not constrain adapter selection. + +- [ ] **Step 4: Regenerate UI metadata from the updated skill** + +Run: + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\generate_openai_yaml.py C:\Users\admin\.codex\skills\daily-summary-job --interface 'display_name=Daily Summary Job' --interface 'short_description=按当前任务生成带领域算法诊断的交互日报' --interface 'default_prompt=使用 $daily-summary-job 理解当前任务和算法,以匹配的领域效果图展示正常机制、问题、影响、修正方案与验证结果,并更新今日日报。' +``` + +Expected: `agents/openai.yaml` contains only the interface block with the three supplied values and valid UTF-8 Chinese. + +- [ ] **Step 5: Validate skill structure and concise loading behavior** + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job +``` + +Expected: `Skill is valid!`. Confirm `SKILL.md` stays under 500 lines and keeps detailed field definitions in `references/report-schema.md`. + +--- + +### Task 7: End-to-end verification on the current task and backward compatibility + +**Files:** + +- Test: all files under `C:\Users\admin\.codex\skills\daily-summary-job` +- Generate temporary outputs only under a verified temporary directory or this project's `dailywork_report` when explicitly updating the real report. + +**Interfaces:** + +- Consumes: completed skill from Tasks 1-6. +- Produces: fresh verification evidence for schema, rendering, interaction hooks, self-containment, task matching, and legacy reports. + +- [ ] **Step 1: Run the complete automated suite** + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py +``` + +Expected: every test passes with zero failures. Record the actual test count; do not reuse the previous count of 31. + +- [ ] **Step 2: Run skill and JavaScript validation** + +```powershell +python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js +node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js +``` + +Expected: skill valid; both JavaScript files exit 0. + +- [ ] **Step 3: Run a temporary legacy report round trip** + +Use the existing `sample_data()` shape without `algorithm_views`. Run `render`, `validate`, and `render --update` in a new temporary project. Expected: Markdown and HTML are created, validate returns `valid: true`, and update reuses the same pair. + +- [ ] **Step 4: Run a temporary current-task domain-view round trip** + +Use the Task 1 `visualization_data()` facts, which match the current path-smoothing work rather than an unrelated generic test. Run `render`, then `validate`. Assert: + +- `valid` is `true`; +- HTML contains the task's actual object IDs and values; +- current, proposed, and verified controls are present; +- verified mode is disabled because this fixture has no verified result; +- Markdown contains the task-specific criterion and missing evidence; +- no external URL, ``, or ` + +``` + +Use text and icons together for status; do not rely on color alone. Provide visible focus states, arrow-key issue navigation, responsive single-column fallbacks, and a no-animation media query. Display “概念示意” whenever a problem lacks numeric evidence. + +- [ ] **Step 4: Implement safe JSON embedding and rendering** + +```python +def safe_json_for_html(data: dict[str, Any]) -> str: + raw = json.dumps(data, ensure_ascii=False, separators=(",", ":")) + return raw.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") + +def render_html(data: dict[str, Any], template: str) -> str: + validate_report_data(data) + if template.count("__REPORT_DATA__") != 1: + raise ValueError("template must contain exactly one report data placeholder") + return template.replace("__REPORT_DATA__", safe_json_for_html(data)) +``` + +- [ ] **Step 5: Run the focused tests** + +Expected: HTML safety, interaction-hook, evidence-consistency, and accessibility-source tests pass. + +--- + +### Task 5: Add checkpoint, render, update, and validate CLI workflows + +**Files:** +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py` + +**Interfaces:** +- Produces CLI subcommands `inspect`, `checkpoint`, `render`, and `validate`. +- All successful commands emit compact JSON to stdout; failures return nonzero with a specific message on stderr. + +- [ ] **Step 1: Add failing end-to-end CLI tests** + +Use `tempfile.TemporaryDirectory` to verify: + +1. `checkpoint` creates one compact JSON under `.daily-summary-job/YYYY-MM-DD/checkpoints`. +2. `render` creates canonical state plus a Markdown/HTML pair under `_rep/YYYY-MM-DD`. +3. `render --update` preserves the original sequence and paths. +4. A second topic receives the next sequence. +5. `validate` rejects mismatched issue identifiers or an external URL in HTML. + +- [ ] **Step 2: Run tests and confirm CLI failures** + +Expected: FAIL because the subcommands are not wired. + +- [ ] **Step 3: Implement the four subcommands** + +- `inspect`: report project root, local date, changed paths, existing report modules, inferred module, and evidence file candidates without writing. +- `checkpoint`: validate compact input, create the checkpoint directory, and write UTF-8 JSON atomically. +- `render`: validate full input, plan or reuse paths, render both outputs to temporary siblings, validate them, atomically replace the pair, and persist canonical state. +- `validate`: compare issue identifiers and evidence levels across canonical JSON, Markdown, and HTML; reject external resources. + +Use `tempfile.NamedTemporaryFile(delete=False, dir=target.parent)` and `Path.replace` only after both staged files pass validation. Clean up staged files in `finally` without deleting existing deliverables. + +- [ ] **Step 4: Run all script tests** + +Run: + +```powershell +python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v +``` + +Expected: all tests pass. + +--- + +### Task 6: Write the concise skill workflow and metadata-aligned instructions + +**Files:** +- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` +- Verify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` + +**Interfaces:** +- Consumes: `scripts/prepare_report.py`, `references/report-schema.md`, and `assets/interactive-report-template.html`. +- Produces: A skill another Codex instance can invoke for record, generate, or update intents without loading unrelated history. + +- [ ] **Step 1: Replace scaffold placeholders with final frontmatter** + +Use only the required YAML keys: + +```yaml +--- +name: daily-summary-job +description: Record compact development checkpoints and generate or update evidence-grounded daily work reports with paired Markdown and self-contained interactive HTML. Use when the user asks to record current development progress, summarize today's coding work, organize problems and improvements, visualize problem/solution reasoning, or update an existing daily development report. +--- +``` + +- [ ] **Step 2: Write the imperative workflow** + +The body must tell the invoking agent to: + +1. Determine record/generate/update intent without requiring fixed wording. +2. Read only current context and today's relevant evidence. +3. Run `inspect` before any write. +4. Preserve evidence boundaries and conflicts. +5. Create the normalized JSON using `report-schema.md`. +6. Use `checkpoint` for compact progress capture. +7. Use `render` for new reports and `render --update` for exact-topic updates. +8. Run `validate` and report precise paths. +9. Never fix business code, run Git commit, fabricate evidence, or read historical days by default. + +- [ ] **Step 3: Verify interface metadata remains aligned** + +`agents/openai.yaml` must show `Daily Summary Job`, the approved Chinese short description, and a default prompt explicitly containing `$daily-summary-job`. Do not add icons, colors, dependencies, or policy fields. + +--- + +### Task 7: Validate the installed skill and run a disposable full workflow + +**Files:** +- Verify only: `C:\Users\admin\.codex\skills\daily-summary-job\**` +- Create and remove only: a dedicated directory under the system temporary directory. + +**Interfaces:** +- Produces: Validation evidence for skill structure, unit behavior, report generation, update stability, and offline HTML constraints. + +- [ ] **Step 1: Run skill structure validation** + +```powershell +python 'C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py' 'C:\Users\admin\.codex\skills\daily-summary-job' +``` + +Expected: validation succeeds. + +- [ ] **Step 2: Run the full script test suite** + +```powershell +python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v +``` + +Expected: all tests pass. + +- [ ] **Step 3: Create a disposable simulated project** + +Create one explicit temporary project containing `src/Map`, `src/PathSmoothing`, and an existing `dailywork_report/pathsmoothing_rep`. Feed a checkpoint and a full report fixture containing achievements, two evidence levels, an improvement, validation results, and next steps. + +- [ ] **Step 4: Run record, generate, update, and validation commands** + +Expected: + +- checkpoint path is date-scoped; +- multi-module input selects `cross-module_rep` unless explicitly overridden; +- generation creates one paired report; +- update keeps the same pair; +- every issue identifier appears in normalized JSON, Markdown, and HTML; +- HTML contains no `http://`, `https://`, external script, or external image reference. + +- [ ] **Step 5: Inspect the final installed file set and repository scope** + +Run: + +```powershell +Get-ChildItem -LiteralPath 'C:\Users\admin\.codex\skills\daily-summary-job' -Recurse -File | Select-Object FullName,Length +git status --short -- 'docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md' 'docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md' +``` + +Expected: only the new skill files exist in the personal directory; the repository shows the two uncommitted documentation files and no task-caused business-code changes. + +## Execution Choice + +The user requested immediate execution without Git commits. Execute this plan inline with `superpowers:executing-plans`; do not dispatch subagents and do not pause for a separate execution-choice prompt. diff --git a/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md b/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md new file mode 100644 index 0000000..c60f19e --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-local-g2-only-path-smoothing-reorganization.md @@ -0,0 +1,106 @@ +# LocalG2-Only PathSmoothing Reorganization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to execute this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert `PathSmoothing` into a LocalG2-only module, remove the three legacy smoothing algorithms, preserve LocalG2 visualization and fixture workflows, and organize the source tree and README using the established `CoarsePath` module pattern. + +**Architecture:** The production facade always runs the LocalG2 pipeline. Shared path preparation and validation remain intact; B-spline, local Bezier, and piecewise quintic implementations and their configuration are removed. Offline reports remain a factual comparison of raw coarse path versus LocalG2 only, with visualization sources placed below an `Output` layer like `CoarsePath`. + +**Tech Stack:** C# 10, .NET SDK, Newtonsoft.Json, existing System.Drawing/StbImageWriteSharp report exporter, PowerShell verification hosts. + +## Global Constraints + +- Do not read, search, enumerate, copy, modify, delete, stage, or commit `ClumsyPilot/ParkrobTrajplanner/auto_avoidance`; do not enumerate `ClumsyPilot/ParkrobTrajplanner` as a parent. +- Preserve LocalG2 candidate construction, validation, publication statuses, fixture data, diagnostic candidate visualization, and generated report artifacts below `ClumsyPilot/obj/path_smoothing_reports`. +- Remove all production references to `CubicBSpline`, `LocalCubicBezier`, and `PiecewiseQuintic` smoothing. +- Retain the raw-path baseline in reports. Normal reports must contain only raw and LocalG2 series and four figures; diagnostic reports may append the already-rejected LocalG2 candidate as a fifth figure. +- Maintain current default `MinimumClearanceReserveMeters = 0d`. +- Do not delete unrelated user work or generated report directories. + +--- + +### Task 1: Establish a LocalG2-only verification contract + +**Files:** +- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` +- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` +- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` + +**Interfaces:** +- The comparison request exposes exactly one requested method: `SmoothingMethod.LocalG2Quintic`. +- A normal report has a raw baseline plus one LocalG2 row/series; the diagnostic report retains its optional rejected candidate figure. + +- [ ] Add failing assertions that reject the three removed enum names, require one requested comparison method, require two normal figure series, and require exactly two CSV rows after the header. +- [ ] Run the focused PowerShell checks and confirm they fail against the four-algorithm implementation. +- [ ] Update host assertions for the new two-series normal report while retaining the seven-file diagnostic contract. +- [ ] Re-run the focused checks after Tasks 2 and 3 and record the exit codes. + +### Task 2: Remove legacy algorithms and simplify the production facade + +**Files:** +- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/` +- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs` +- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs` +- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs` + +**Interfaces:** +- `SmoothingMethod` retains only `LocalG2Quintic`. +- `PathSmoothingConfiguration` defaults `Method` to `LocalG2Quintic` and exposes only shared safety/sampling fields and `LocalG2Quintic` options. +- `PathSmoothingService.Smooth(request, cancellationToken)` directly validates/prepares/builds the raw baseline and invokes `LocalG2PreSmoothingPipeline`. + +- [ ] Delete legacy source files only after their callers are removed. +- [ ] Remove legacy smoothness/retry configuration and cloning code; preserve output spacing, collision step, clearance reserve, and LocalG2 options. +- [ ] Replace the multi-method resolver and fallback path in `PathSmoothingService` with its LocalG2-only route. +- [ ] Compile the isolated PathSmoothing host and confirm no source references to the removed methods remain in allowed paths. + +### Task 3: Reorganize report sources into an Output layer and reduce the report model + +**Files:** +- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/` +- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/` +- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingMethodRanker.cs` +- Modify: moved comparison request/result/service consumers and all moved visualization files. + +**Interfaces:** +- `PathSmoothingComparisonRequest` owns one immutable LocalG2 request rather than a caller-selectable method list. +- `PathSmoothingComparisonResult` contains a raw baseline and exactly one LocalG2 entry. +- Normal figure and CSV builders emit `RawPath` and `LocalG2Quintic` only. + +- [ ] Move source directories with their namespaces changed from `PathSmoothing.Comparison` and `PathSmoothing.Visualization` to `PathSmoothing.Output.Comparison` and `PathSmoothing.Output.Visualization`. +- [ ] Simplify comparison execution to warm up and measure LocalG2 only; retain deterministic timing/digest behavior for its sole entry. +- [ ] Remove visual style colors, legend rows, labels, metric rows, and all source references for the three deleted algorithms. +- [ ] Publish four normal figures with stable stems `01-coarse-path-overview`, `02-all-paths-comparison`, `03-local-g2-overview`, and `04-curvature-comparison`; append `05-local-g2-diagnostic-candidate` only to an augmented diagnostic model. +- [ ] Update all allowed source/test imports to the new `Output` namespaces. + +### Task 4: Align test/demo entry points and document the module + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md` + +**Interfaces:** +- Fixture reports use the LocalG2-only comparison request and retain all eight fixtures. +- The README mirrors the `CoarsePath/README.md` information architecture for LocalG2 inputs, safety gates, result statuses, report output, and known limitations. + +- [ ] Update test/demo imports and expected report shapes for the Output namespaces and LocalG2-only model. +- [ ] Create `README.md` with the following ordered sections: Module Overview, File Structure, Smoothing Data Flow, Result Status and Publication Rules, Coordinates and Units, Minimal Call Example, Detailed Usage Guide, Fixture Reports and Visualization, Common Errors, and First-Version Limits. +- [ ] State explicitly that a candidate passing collision validation may still be retained when its quality gate fails, and that `0 m` reserve removes only the additional clearance reserve, not collision or curvature checks. + +### Task 5: Verify source layout and retain visualization artifacts + +**Files:** +- Verify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/` +- Verify: `ClumsyPilot/obj/path_smoothing_reports/` + +- [ ] Build and run the isolated current-source LocalG2 visualization host against all eight fixture scenarios. +- [ ] Confirm all normal report directories contain the expected four PNG/SVG figures and CSV, and that `02-all-paths-comparison.png` presents raw plus LocalG2 only. +- [ ] Run the focused comparison/SVG/diagnostic verification scripts where their dependencies are available; report any root-build limitation separately. +- [ ] Inspect at least the `single-turn` normal report and `05-local-g2-diagnostic-candidate.png` to confirm LocalG2 labels, nonblank rendering, and retained diagnostic semantics. +- [ ] Update `.superpowers/sdd/progress.md` with the actual cleanup results and verification evidence. diff --git a/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md b/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md new file mode 100644 index 0000000..4ea3973 --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-em-observation-movementtest.md @@ -0,0 +1,457 @@ +# EM Observation MovementTest Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. + +**Goal:** Build a real-localization, observe-only MovementTest that creates a configurable start/goal map, plans Hybrid A* → Local G2 → EM trajectories, and shows world, LS, and ST diagnostics without sending a chassis command. + +**Architecture:** Put map construction, planning bootstrap, rolling EM requests, trajectory observation, and LS/ST derivation in pure, testable classes. Keep MDCS reads, prompts, painters, background timing, and cancellation in one thin MovementTest host. The host may only read DetourInterface and BasicPilotBase.Chassis; its only control output is a displayed TrajectoryControlCommand. + +**Tech Stack:** C# 10, netstandard2.0, existing Clumsy MovementTest/Painter UI, MDCS localization and chassis read APIs, Hybrid A*, Local G2, EM planner, OSQP, and EMPlannerVerificationHost. + +## Global Constraints + +- All map geometry and UI world coordinates are mm; Pose2D, velocities, and EM geometry are m, m/s, and rad. +- Bounds are exactly the start/goal axis-aligned rectangle expanded by MapPaddingMeters on all sides. Obstacles must fit these bounds; they must not enlarge them. +- Default settings are: padding 2.0 m, resolution 50 mm, replan 0.20 s, observer period 0.05 s. +- Capture world pose through DetourInterface.getCartLocation() and signed body-longitudinal velocity from BasicPilotBase.Chassis.GetCarSpeed(true).Vx. Create a monotonically increasing state sequence id. +- Output is TrajectoryControlCommand for display only. Do not invoke SendXYThSpeed, SendMotion, SendTh, AccumulateSpeed, ComputeWheelsGeometrically, brake/wheel adapter methods, or a geometric controller. +- Keep runtime source under ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest. Do not change the existing coarse-path factory, whose unrelated manual demo uses an 8 m expansion. +- Stop/cancel must cancel worker activity and clear the World, LS, and ST painter layers. + +--- + +## File structure + +| File | Responsibility | +| --- | --- | +| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs | Settings, manual-obstacle DTOs, validation, exact map-job construction. | +| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs | Hybrid A* + Local G2 bootstrap, rolling EM requests, time observation, LS/ST models. | +| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs | Three painter layers and presentation text; no MDCS/hardware use. | +| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs | Discoverable test, MDCS state reader, prompts, background session, console, cancellation. | +| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md | Operator configuration, layer interpretation, unit and safety guidance. | +| ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs | Deterministic regression checks and an actuator-call source audit. | +| ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs | Adds the trajectory-observation command. | + +### Task 1: Configuration and exact rectangle-map inputs + +**Files:** + +- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs +- Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs + +**Interfaces:** + +- Consumes: Pose2D, VehicleParameters, PlanningMapRequest, MapBoundsMm, ManualObstacleSource, CircleObstacle, AxisAlignedRectangleObstacle. +- Produces: TrajectoryObservationSettings.Validate(), TrajectoryObservationObstacle.Circle(double, double, double), TrajectoryObservationObstacle.Rectangle(double, double, double, double), and TrajectoryObservationSetupFactory.CreateBootstrapJob(Pose2D, Pose2D, TrajectoryObservationSettings, IReadOnlyList, long). + +- [ ] **Step 1: Write failing map-bounds and obstacle checks** + +Create the verification host class and invoke it with a new trajectory-observation argument: + +~~~csharp +internal static class TrajectoryObservationChecks +{ + public static void Run() + { + VerifiesStartGoalBoundsUseOnlyConfiguredPadding(); + RejectsObstacleOutsideConfiguredBounds(); + } + + private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding() + { + var settings = new TrajectoryObservationSettings + { + MapPaddingMeters = 2d, + MapResolutionMillimeters = 50f, + }; + CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( + new Pose2D(10d, -5d, 0d), new Pose2D(13d, -1d, 0d), settings, + Array.Empty(), 17L); + + Verification.NearlyEqual(8000d, job.MapRequest.Bounds.XMin, "observer map x min"); + Verification.NearlyEqual(15000d, job.MapRequest.Bounds.XMax, "observer map x max"); + Verification.NearlyEqual(-7000d, job.MapRequest.Bounds.YMin, "observer map y min"); + Verification.NearlyEqual(1000d, job.MapRequest.Bounds.YMax, "observer map y max"); + Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution"); + } +} +~~~ + +Modify Program.Main to accept trajectory-observation, call TrajectoryObservationChecks.Run(), then write PASS trajectory-observation. Add the same call to em-all. + +- [ ] **Step 2: Run the new check to prove it fails** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +~~~ + +Expected: compilation fails because TrajectoryObservationSettings and TrajectoryObservationSetupFactory do not exist. + +- [ ] **Step 3: Implement the contracts and factory** + +Create the editable configuration contract: + +~~~csharp +public sealed class TrajectoryObservationSettings +{ + public double MapPaddingMeters { get; set; } = 2d; + public float MapResolutionMillimeters { get; set; } = 50f; + public double ReplanPeriodSeconds { get; set; } = 0.20d; + public double ObserverPeriodSeconds { get; set; } = 0.05d; + public double VehicleLengthMeters { get; set; } = 0.80d; + public double VehicleWidthMeters { get; set; } = 0.60d; + public double SafetyMarginMeters { get; set; } = 0.05d; + public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d; + + public void Validate(); + public VehicleParameters CreateVehicle(); +} +~~~ + +Implement finite/positive validation. Implement the obstacle as world-mm circle or axis-aligned rectangle with GetBounds() and ToMapObstacle(). Build bounds with the following exact calculation, rounded outward to the configured grid: + +~~~csharp +double padMm = settings.MapPaddingMeters * 1000d; +var bounds = new MapBoundsMm( + ToGridLower(Math.Min(start.X, goal.X) * 1000d - padMm, settings.MapResolutionMillimeters), + ToGridUpper(Math.Max(start.X, goal.X) * 1000d + padMm, settings.MapResolutionMillimeters), + ToGridLower(Math.Min(start.Y, goal.Y) * 1000d - padMm, settings.MapResolutionMillimeters), + ToGridUpper(Math.Max(start.Y, goal.Y) * 1000d + padMm, settings.MapResolutionMillimeters)); +~~~ + +Reject an obstacle unless its full envelope is contained in bounds. With zero obstacles set AllowExplicitEmptyMap true. Otherwise construct exactly one required ManualObstacleSource named trajectory-observer-manual with the supplied positive snapshot version. Return a CoarsePathPlanningJob with new HybridAStarConfiguration, StartDirection = null, and GoalDirection = GoalDirectionConstraint.Any. + +- [ ] **Step 4: Run focused and existing checks** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation +~~~ + +Expected: both exit 0 and print PASS trajectory-observation and PASS foundation. + +- [ ] **Step 5: Commit the input layer** + +~~~powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs +git commit -m "feat: add observation test map inputs" +~~~ + +### Task 2: Pure planning bootstrap, time observation, and LS/ST derivation + +**Files:** + +- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs +- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs + +**Interfaces:** + +- Consumes: CoarsePathPlanningService, PathSmoothingService, EmPlanningCoordinator, TrajectoryExecutor, FrenetProjector, and caller-supplied VehicleMotionState. +- Produces: TrajectoryObservationBootstrapper.Bootstrap(CoarsePathPlanningJob, CancellationToken), TrajectoryObservationController.StartCycle(DateTimeOffset, VehicleMotionState, CancellationToken), TrajectoryObservationController.Observe(DateTimeOffset, VehicleMotionState), and TrajectoryObservationCharts.Build(EmTrajectory, DirectionSegmentView, double). + +- [ ] **Step 1: Add failing chart and time-sampling checks** + +Extend TrajectoryObservationChecks.Run() by adding VerifiesLsAndStUsePublishedTrajectoryData(). Use a fixed two-point EmTrajectory whose EffectiveAtUtc is 2026-08-04T00:00:00Z, with TimeFromStart values 0 and 1, PathS values 4 and 5, and known signed speeds. Assert that Build returns two ST samples (0,4) and (1,5), two speed samples, and the expected LS projection count. Call Observe at 00:00:00.500Z and assert that TrajectoryExecutor selected an interpolated point with TimeFromStart == 0.5d. + +- [ ] **Step 2: Run the new check to prove it fails** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +~~~ + +Expected: compilation fails because TrajectoryObservationCharts and TrajectoryObservationController do not exist. + +- [ ] **Step 3: Implement the pipeline** + +Bootstrap must use exactly this success gate: + +~~~csharp +CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken); +if (coarse.PlanningResult.Status != PlanningStatus.Success) + return TrajectoryObservationBootstrapResult.FromFailure( + job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status); + +var smoothingRequest = new PathSmoothingRequest( + CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map), + coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle, + new PathSmoothingConfiguration()); +PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken); +if (!IsPublishedSmoothingStatus(smooth.Status)) + return TrajectoryObservationBootstrapResult.FromFailure( + job, coarse, smooth, smooth.Diagnostics.TerminationReason); +return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth)); +~~~ + +IsPublishedSmoothingStatus accepts only Complete, PartialImprovement, NotNeeded, and Unchanged. CopyFiniteClearance replaces a positive-infinite clearance with the finite map diagonal before copying each CoarsePathPoint. + +TrajectoryObservationController owns EmPlanningCoordinator and TrajectoryExecutor. For a replan it creates: + +~~~csharp +var request = new EmPlanningRequest( + bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Job.Vehicle, state, configuration, + segmentIndex, coordinator.PublishedTrajectory, now, now, + sessionId + "-trajectory-" + cycleId, sessionId + "-reference", + coordinator.PublishedTrajectory?.Metadata.TrajectoryId ?? string.Empty, + EmMotionModel.NonholonomicForwardReverse); +return coordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken); +~~~ + +Set configuration.Scheduling.ReplanPeriodSeconds from settings. Initial observation mode always uses segmentIndex 0. Observe must use PublishedTrajectory only; when non-null call UpdateCommand(now, state, trajectory, trajectory.Metadata.Direction, trajectory.Metadata.Direction, true) and return the selected point, command, and executor state for display only. + +Build LS/ST from published data alone: + +~~~csharp +ls.Add(new TrajectoryObservationLsSample( + segment.SourceStartArcLength + projection.ReferenceS, projection.LateralOffset)); +st.Add(new TrajectoryObservationStSample(point.TimeFromStart, point.PathS)); +speed.Add(new TrajectoryObservationSpeedSample(point.TimeFromStart, point.SignedLongitudinalVelocity)); +~~~ + +Use seeded FrenetProjector calls and count failed projections. No pipeline class may reference UI, DetourInterface, BasicPilotBase, or a hardware class. + +- [ ] **Step 4: Run diagnostics and regression checks** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor +~~~ + +Expected: every command exits 0. + +- [ ] **Step 5: Commit the pure pipeline** + +~~~powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: add EM observation planning pipeline" +~~~ + +### Task 3: Presentation layers and observation text + +**Files:** + +- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs +- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs + +**Interfaces:** + +- Consumes: bootstrap result, observation result, and chart data. +- Produces: TrajectoryObservationPresentation.DrawWorld(TrajectoryObservationBootstrapResult, TrajectoryObservationObservation), DrawLs(TrajectoryObservationCharts), DrawSt(TrajectoryObservationCharts), ClearAll(), and TrajectoryObservationPresentationText.Create(TrajectoryObservationObservation, TrajectoryObservationCharts). + +- [ ] **Step 1: Add a failing presentation-text check** + +Assert that TrajectoryObservationPresentationText.Create(observation, charts) contains the literal OBSERVE_ONLY: no chassis command is sent., selected point time/path-S, signed speed, yaw rate, and LS projection failure count. The check must not instantiate a Painter. + +- [ ] **Step 2: Run the check to verify it fails** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +~~~ + +Expected: compilation fails because TrajectoryObservationPresentationText does not exist. + +- [ ] **Step 3: Implement the three painters** + +Create exactly these named layers: + +~~~csharp +worldPainter = UI.GetPainter("TrajectoryObserver.World", true); +lsPainter = UI.GetPainter("TrajectoryObserver.LS", true); +stPainter = UI.GetPainter("TrajectoryObserver.ST", true); +~~~ + +DrawWorld clears only worldPainter then draws map bounds/grid/occupied cells, start, goal, coarse path, Local G2 path, real pose, and latest EM path. Convert every planner position from m to mm before calling DrawLine, DrawCircle, or DrawText. + +DrawLs draws axes plus s-l samples. DrawSt draws t-s and a vertically separated t-v series with a legend. A missing trajectory draws a status string instead of throwing. ClearAll invokes Clear on all three painters and performs no other action. + +- [ ] **Step 4: Run visual-model and compile verification** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore +~~~ + +Expected: trajectory-observation passes and the project has zero compile errors. + +- [ ] **Step 5: Commit presentation** + +~~~powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: visualize EM observation diagnostics" +~~~ + +### Task 4: MDCS read-only MovementTest host + +**Files:** + +- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs +- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md +- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs + +**Interfaces:** + +- Consumes: DetourInterface.getCartLocation(), BasicPilotBase.Chassis.GetCarSpeed(true), setup/controller/presentation APIs. +- Produces: a [MovementTest(name = "EM轨迹规划观察闭环测试")] entry with Test() and TestStop(). + +- [ ] **Step 1: Write a failing actuator-free source audit** + +Add VerifiesObservationSourceHasNoActuatorCalls() to TrajectoryObservationChecks.Run() and implement it in the verification host. It reads the observation runtime source files and fails on any of these tokens: + +~~~csharp +new[] +{ + ".SendXYThSpeed(", ".SendMotion(", ".SendTh(", ".AccumulateSpeed(", + ".ComputeWheelsGeometrically(", ".DriveStop(", ".PredefinedDriveStop(" +} +~~~ + +The audit strings live only in the test host; none may appear in the new runtime observation files. + +- [ ] **Step 2: Run the audit before the host exists** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +~~~ + +Expected: the check fails because MovementTest.TrajectoryObservationTest.cs is missing. + +- [ ] **Step 3: Implement the host and lifecycle** + +Use this discoverable configuration: + +~~~csharp +[MovementTest(name = "EM轨迹规划观察闭环测试")] +public sealed class TrajectoryObservationMovementTest : MovementTest +{ + public double GoalXmm = double.NaN; + public double GoalYmm = double.NaN; + public double GoalYawDeg = 0d; + public double MapPaddingMeters = 2d; + public float MapResolutionMm = 50f; + public double ReplanPeriodSeconds = 0.20d; + public double ObserverPeriodSeconds = 0.05d; + + public override void Test(); + public override void TestStop(); +} +~~~ + +When GoalXmm or GoalYmm is non-finite, prompt for all goal values with the same finite parser/UI.GetInput pattern as CoarsePathPlanningTest. Prompt for 0–20 manual obstacles (circle or rectangle) and freeze all inputs before Task.Run begins. + +The MDCS reader must use only this read path: + +~~~csharp +var location = DetourInterface.getCartLocation(); +if (location == null) throw new InvalidOperationException("Live localization is unavailable."); +if (BasicPilotBase.Chassis == null) throw new InvalidOperationException("Live chassis read interface is unavailable."); +var speed = BasicPilotBase.Chassis.GetCarSpeed(true); +return new VehicleMotionState( + new Pose2D(location.x / 1000d, location.y / 1000d, location.th * Math.PI / 180d), + speed.Vx, null, DateTimeOffset.UtcNow, Interlocked.Increment(ref stateSequence)); +~~~ + +Bootstrap once in a cancellable Task.Run. After success, run Task.Delay(TimeSpan.FromSeconds(ObserverPeriodSeconds), token) between ticks. At each tick capture exactly one state, start a cycle only when controller.ShouldStartCycle(now), observe the latest published trajectory, draw all layers, and print a throttled status. Construct the service as new EmPlanningService(new OsqpNativeSolver()). + +Every status includes: + +~~~text +OBSERVE_ONLY: no chassis command is sent. +~~~ + +When a GearSwitch trajectory reaches its final time, draw and print 等待真实档位/方向确认;观察模式不会推进下一方向段, leave segment index 0, and do not create a direction-change action. Goal and rolling-stop commands may only be logged. + +Use a lock/session id pattern matching CoarsePathPlanningTestRunner: replace the active CancellationTokenSource, cancel the old source without waiting, and allow only the current session to draw or log. TestStop cancels, disposes after task completion, and calls presentation.ClearAll. + +Write README.md with configuration fields/units, obstacle examples, default values, chart interpretations, the VelocityX/VelocityY world-frame warning, and the explicit no-driving limitation. + +- [ ] **Step 4: Verify runner safety and integration build** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore +rg -n 'SendXYThSpeed\(|SendMotion\(|SendTh\(|AccumulateSpeed\(|ComputeWheelsGeometrically\(|DriveStop\(|PredefinedDriveStop\(' ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -g '*.cs' +~~~ + +Expected: host and build exit 0. The rg command exits 1 because no runtime observation file calls a forbidden actuator method. + +- [ ] **Step 5: Commit the MovementTest** + +~~~powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: add read-only EM observation movement test" +~~~ + +### Task 5: End-to-end regression and operator handoff + +**Files:** + +- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md + +**Interfaces:** + +- Consumes: completed observation-test components and existing EMPlannerVerificationHost checks. +- Produces: a reproducible all-up verification command and a launch/stop checklist. + +- [ ] **Step 1: Add a failing bootstrap regression** + +Use an empty-map setup with start (0.5, 0.5, 0) m and goal (3.5, 0.5, 0) m. Assert that bootstrap returns a successful map, PlanningStatus.Success, a publishable smoothing result, and at least one DirectionSegmentView. This check does not run native OSQP. + +- [ ] **Step 2: Run the check to confirm its failure** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +~~~ + +Expected: the assertion identifies a missing or incorrect bootstrap result. + +- [ ] **Step 3: Make the smallest corrective change** + +Correct only TrajectoryObservationSetupFactory or TrajectoryObservationBootstrapper so the empty-map request produces a planning-ready map and publishable Local G2 path. Preserve the exact bounds rule and do not add UI, MDCS, or hardware dependencies to pure classes. + +- [ ] **Step 4: Run all required evidence checks** + +Run: + +~~~powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all +dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore +git diff --check +~~~ + +Expected: every command exits 0. In the vehicle UI, the entry appears as EM轨迹规划观察闭环测试 and starting/running/stopping it does not issue any chassis, motor, steering, or brake output. + +- [ ] **Step 5: Commit final verification/documentation** + +~~~powershell +git add -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md +git commit -m "test: verify EM observation movement test" +~~~ + +## Plan self-review + +**Spec coverage:** Task 1 provides configurable start/goal map bounds, vehicle settings, and manual obstacles. Task 2 covers Hybrid A*, Local G2, rolling EM, time sampling, and derived LS/ST. Task 3 creates World/LS/ST painters. Task 4 reads live MDCS state, prints observation diagnostics, handles gear-switch observation, and ensures cancellation/no-write behavior. Task 5 supplies an end-to-end fixture and final evidence. + +**Placeholder scan:** Every task names concrete files, commands, interface names, inputs, expected behavior, and commit content; no deferred implementation markers remain. + +**Type consistency:** Map code produces PlanningMapRequest and CoarsePathPlanningJob; bootstrap produces PathSmoothingResult and DirectionSegmentView; rolling planning consumes VehicleMotionState and EmPlanningRequest; UI consumes EmTrajectory, TrajectoryControlCommand, and chart samples without changing EM contracts. diff --git a/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md b/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md new file mode 100644 index 0000000..56131e8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md @@ -0,0 +1,334 @@ +# EM Observation Diagnostics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make every EM planning-cycle failure visible in the host terminal and the World/L-S/S-T observation canvases without changing observation-only safety behavior. + +**Architecture:** Add a pure diagnostic formatter that keeps `EmPlanningStatus` and the original `FailureReason` from `PlanningCycleResult`. Extend the observation-loop tick with explicit start/completion events so the runner writes one pending line and one completed-cycle line per cycle, while the painters receive the current diagnostic every tick. + +**Tech Stack:** C# / .NET Standard 2.0 plugin, `Hedingben.ToastText`, `UI.GetPainter`, EM planner contracts, .NET verification host. + +## Global Constraints + +- The MovementTest remains observe-only: do not add any chassis, brake, wheel, or actuator call. +- Terminal output uses `Console.WriteLine` and starts with `[TrajectoryObserver]`. +- Every completed cycle reports raw `EmPlanningStatus`, `published`, version, elapsed time, and the original nonempty `FailureReason`. +- A 50 ms observer tick must not emit a duplicate terminal record. +- World, L-S, and S-T painters must show the diagnostic even when `PublishedTrajectory` is null. + +--- + +### Task 1: Add a pure planning diagnostic formatter + +**Files:** +- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` + +**Interfaces:** +- Consumes: `PlanningCycleResult`, `EmTrajectory`, `TimeSpan`, and the in-flight flag. +- Produces: `TrajectoryObservationDiagnostic.Text`, a compact multi-line operator string. + +- [ ] **Step 1: Write the failing test** + +Add `VerifiesPlanningDiagnosticsKeepRawFailureReason();` to `Run()`, then add: + +```csharp +private static void VerifiesPlanningDiagnosticsKeepRawFailureReason() +{ + var failed = new PlanningCycleResult( + 4L, + new PlanningCycleIdentity(3L, "diagnostic-reference", 7L, string.Empty, 0), + new EmPlanningResult(EmPlanningStatus.CorridorInfeasible, null, + "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"), + false, + "map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"); + + TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create( + failed, TimeSpan.FromMilliseconds(18d), false, null); + + Verification.True(diagnostic.Text.Contains("cycle=4"), "diagnostic has cycle version"); + Verification.True(diagnostic.Text.Contains("status=CorridorInfeasible"), "diagnostic preserves raw status"); + Verification.True(diagnostic.Text.Contains("published=False"), "diagnostic preserves publish state"); + Verification.True(diagnostic.Text.Contains("elapsed=18ms"), "diagnostic preserves elapsed time"); + Verification.True(diagnostic.Text.Contains("reason=map=3;reference=diagnostic-reference"), + "diagnostic preserves planner failure reason"); + + TrajectoryObservationDiagnostic pending = TrajectoryObservationDiagnostics.Create( + null, TimeSpan.Zero, true, null); + Verification.Equal("planning status=pending", pending.Text, "diagnostic reports pending before completion"); +} +``` + +- [ ] **Step 2: Run the test to verify RED** + +Run: + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +``` + +Expected: build failure because `TrajectoryObservationDiagnostic` and `TrajectoryObservationDiagnostics` do not exist. + +- [ ] **Step 3: Write the minimal formatter** + +Create `TrajectoryObservationDiagnostics.cs`: + +```csharp +using System; +using System.Globalization; +using MultiWheelC.TrajectoryPlanning.EMPlanner; + +namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; + +public sealed class TrajectoryObservationDiagnostic +{ + public TrajectoryObservationDiagnostic(string text) + { + Text = text ?? string.Empty; + } + + public string Text { get; } +} + +public static class TrajectoryObservationDiagnostics +{ + public static TrajectoryObservationDiagnostic Create(PlanningCycleResult latestCycle, + TimeSpan elapsed, bool planningInFlight, EmTrajectory publishedTrajectory) + { + if (latestCycle == null) + return new TrajectoryObservationDiagnostic(planningInFlight + ? "planning status=pending" + : "planning status=not-started"); + + string text = "planning cycle=" + latestCycle.Version.ToString(CultureInfo.InvariantCulture) + + " status=" + latestCycle.Result.Status + + " published=" + latestCycle.Published + + " elapsed=" + Math.Max(0d, elapsed.TotalMilliseconds).ToString("F0", CultureInfo.InvariantCulture) + "ms"; + if (planningInFlight) + text += "\nreplan=pending"; + if (publishedTrajectory != null) + text += "\ntrajectory=" + publishedTrajectory.Metadata.TrajectoryId; + if (!string.IsNullOrWhiteSpace(latestCycle.Result.FailureReason)) + text += "\nreason=" + latestCycle.Result.FailureReason; + return new TrajectoryObservationDiagnostic(text); + } +} +``` + +- [ ] **Step 4: Run the test to verify GREEN** + +Run the Step 2 command. + +Expected: `PASS trajectory-observation`. + +- [ ] **Step 5: Commit** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: format EM observation diagnostics" +``` + +### Task 2: Report once at the start and completion of every planning cycle + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` + +**Interfaces:** +- Produces: `TrajectoryObservationLoopTick.PlanningStarted` and `.PlanningCompleted`. +- Consumes: those flags in the MovementTest to issue one terminal/UI status record per lifecycle event. + +- [ ] **Step 1: Write the failing test** + +In `VerifiesObserverTicksWhilePlanningIsDelayed`, after the first tick, add: + +```csharp +Verification.True(firstTick.PlanningStarted, "observer first tick reports a planning-cycle start"); +Verification.True(!firstTick.PlanningCompleted, "observer first tick has no completed cycle"); +``` + +After `finalTick` is created, add: + +```csharp +Verification.True(finalTick.PlanningCompleted, "observer completion tick reports cycle completion"); +``` + +In `VerifiesObservationSourceUsesRequiredOperatorText`, add: + +```csharp +Verification.True(source.Contains("Console.WriteLine(\"[TrajectoryObserver] \" + text);"), + "observer status is mirrored to the host terminal"); +``` + +- [ ] **Step 2: Run the test to verify RED** + +Run the Task 1 test command. + +Expected: assertions fail because lifecycle flags and terminal output do not exist. + +- [ ] **Step 3: Implement lifecycle flags and output** + +Change `TrajectoryObservationLoopTick` to accept and expose: + +```csharp +bool planningStarted, bool planningCompleted + +public bool PlanningStarted { get; } + +public bool PlanningCompleted { get; } +``` + +In `TrajectoryObservationLoop.Tick`, use: + +```csharp +bool planningCompleted = ConsumeCompletedPlanning(now); +bool planningStarted = false; +if (planningTask == null && controller.ShouldStartCycle(now)) +{ + planningStarted = true; + planningStartedAtUtc = now; + planningTask = controller.StartCycle(now, state, cancellationToken); + planningCompleted |= ConsumeCompletedPlanning(now); +} +``` + +Change `ConsumeCompletedPlanning` to return `false` when no completed Task is available and `true` after it assigns `latestCycle`, updates elapsed time, and clears `planningTask`. Pass both flags to the tick constructor. + +In `RunSessionAsync`, after the tick is created, make one diagnostic and only log event records: + +```csharp +TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create( + tick.LatestCycle, tick.LatestPlanningElapsed, tick.PlanningInFlight, + observation.PublishedTrajectory); +if (tick.PlanningStarted) + LogIfCurrent(sessionId, "planning status=pending"); +if (tick.PlanningCompleted) + LogIfCurrent(sessionId, diagnostic.Text); +``` + +Remove the unconditional `if (tick.ShouldLog)` status call. Keep the existing session-start, stop, bootstrap-failure, and runtime-fault calls. + +Change `PrintStatus` to: + +```csharp +private static void PrintStatus(string message) +{ + string text = ObserveOnlyNotice + "\n" + message; + Hedingben.ToastText(text, StatusChannel); + Console.WriteLine("[TrajectoryObserver] " + text); +} +``` + +- [ ] **Step 4: Run the test to verify GREEN** + +Run the Task 1 test command. + +Expected: `PASS trajectory-observation`; the delayed-planner test still proves the observer does not wait for planning. + +- [ ] **Step 5: Commit** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: report EM observation planning cycles" +``` + +### Task 3: Persist planning diagnostics in all three painter layers + +**Files:** +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` + +**Interfaces:** +- Consumes: `TrajectoryObservationDiagnostic.Text`. +- Produces: World/L-S/S-T empty states that show the precise planning status and reason. + +- [ ] **Step 1: Write the failing test** + +Add `VerifiesEmptyChartsReceivePersistentPlanningDiagnostic();` to `Run()` and add: + +```csharp +private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic() +{ + string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", + "ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs"); + string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath)); + + Verification.True(source.Contains("DrawLs(TrajectoryObservationCharts charts, string diagnosticText)"), + "LS painter accepts planning diagnostic input"); + Verification.True(source.Contains("DrawSt(TrajectoryObservationCharts charts, string diagnosticText)"), + "ST painter accepts planning diagnostic input"); + Verification.True(source.Contains("No published trajectory available for L-S chart.\n"), + "LS empty state includes diagnostic after chart label"); + Verification.True(source.Contains("No published trajectory available for T-S/T-V charts.\n"), + "ST empty state includes diagnostic after chart label"); +} +``` + +- [ ] **Step 2: Run the test to verify RED** + +Run the Task 1 test command. + +Expected: source checks fail because painter methods have no diagnostic parameter. + +- [ ] **Step 3: Add painter parameters and wire the diagnostic** + +Change signatures to: + +```csharp +public void DrawWorld(TrajectoryObservationBootstrapResult bootstrap, + TrajectoryObservationObservation observation, TrajectoryObservationRuntimeState runtimeState, + string diagnosticText) + +public void DrawLs(TrajectoryObservationCharts charts, string diagnosticText) + +public void DrawSt(TrajectoryObservationCharts charts, string diagnosticText) +``` + +Add this helper in `TrajectoryObservationPresentation`: + +```csharp +private static string EmptyChartMessage(string label, string diagnosticText) +{ + return string.IsNullOrWhiteSpace(diagnosticText) + ? label + : label + "\n" + diagnosticText; +} +``` + +For World, draw `EmptyChartMessage("No published trajectory available.", diagnosticText)` at `bootstrap.Map.Bounds.XMin + 100f, bootstrap.Map.Bounds.YMin + 300f` before returning from the empty trajectory path. For L-S and S-T, draw `EmptyChartMessage` with their existing label at `0f, 0f`. + +Change `DrawIfCurrent` to accept `TrajectoryObservationDiagnostic diagnostic` and call: + +```csharp +Presentation.DrawWorld(bootstrap, observation, runtimeState, diagnostic?.Text ?? string.Empty); +Presentation.DrawLs(charts, diagnostic?.Text ?? string.Empty); +Presentation.DrawSt(charts, diagnostic?.Text ?? string.Empty); +``` + +Pass the diagnostic created in Task 2 from `RunSessionAsync`. For the bootstrap-failure path, pass: + +```csharp +new TrajectoryObservationDiagnostic("bootstrap failed: " + bootstrap.FailureReason) +``` + +- [ ] **Step 4: Verify focused test, build, and diff** + +Run: + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore +git diff --check +``` + +Expected: `PASS trajectory-observation`, zero build errors, and no diff whitespace errors. + +- [ ] **Step 5: Commit** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +git commit -m "feat: show EM observation failure diagnostics" +``` + diff --git a/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md b/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md new file mode 100644 index 0000000..6d2a90e --- /dev/null +++ b/docs/superpowers/plans/2026-08-09-em-full-direction-correctness-fixes.md @@ -0,0 +1,227 @@ +# EM FullDirection Correctness Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use test-driven-development for every task. Execute tasks serially because they share the EM solver pipeline. + +**Goal:** Fix confirmed FullDirection projection, cancellation, curvature-constraint, timeout-budget, and trajectory-coordinate defects without replacing the existing LS/ST planner. + +**Architecture:** Keep `IEmPlanningService`, `EmPlanningRequest`, `LateralPlanner`, `LongitudinalPlanner`, and trajectory contracts compatible. Apply local fixes where one component owns the invariant; introduce only a shared internal lateral-curvature affine model and internal explicit-budget overloads where the same invariant necessarily crosses components. + +**Tech Stack:** C# 10, .NET Standard 2.0 production assembly, .NET 8 verification host, solver-neutral `IQpSolver` tests. + +## Global Constraints + +- `FullDirectionSegment` plans exactly one complete direction segment; `RollingHorizon` behavior is not redesigned in this plan. +- FullDirection ego admission is restricted to the segment-start prefix `[0, min(L, MaximumProjectionDistanceMeters)]`; it must not select a later U-shape/self-overlap branch. +- A start heading error with magnitude greater than or equal to `π/2` is rejected before `tan(headingError)` is evaluated. +- `Cancelled` always carries a null trajectory/path/candidate, even if a strict fallback candidate exists. +- `SolverTimeoutSeconds` is one combined LS+ST solve budget for a service call, not a fresh budget for each optimizer. +- Every LS QP has a finite linearized vehicle-curvature hard-bound row at every station; nonlinear validation remains authoritative. +- `SegmentLocalS` stores interpolated direction-segment reference S; `PathS` stores optimized lateral-path arc length. +- Do not edit or revert the user's existing `EmPlannerConfiguration.cs` change, PathSmoothing work, Map work, or `ClumsyPilot.csproj` changes. +- Do not add actuator calls, change public EM request/result signatures, or commit/stage files from the dirty shared worktree. + +--- + +### Task 1: Anchor FullDirection start projection and reject folded headings + +**Files:** +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` + +**Interfaces:** +- Consumes: existing `FrenetProjector.TryProject` bounded-window overload. +- Produces: service-local FullDirection start-prefix admission; Rolling continues using `[0,L]`. + +- [ ] **Step 1: Write failing service tests.** Add a FullDirection U-shaped all-forward reference whose later arm is closer to the measured pose, and assert `ProjectionFailed` rather than accepting a later `ReferenceS`. Add a same-position start pose with yaw `π`, and assert `ProjectionFailed` with a null trajectory. + +```csharp +EmPlanningResult wrongBranch = service.Plan(fullURequest, CancellationToken.None); +Verification.Equal(EmPlanningStatus.ProjectionFailed, wrongBranch.Status, + "FullDirection cannot enter through a later U branch"); + +EmPlanningResult reversedHeading = service.Plan(oppositeHeadingRequest, CancellationToken.None); +Verification.Equal(EmPlanningStatus.ProjectionFailed, reversedHeading.Status, + "opposite start heading is rejected before slope conversion"); +``` + +- [ ] **Step 2: Run the focused test and verify RED.** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service +``` + +Expected: the later U branch and/or opposite-heading assertion fails under the current global `[0,L]`, seed-zero projection. + +- [ ] **Step 3: Implement the local admission rule.** In `EmPlanningService.Plan`, choose the projection upper bound from scope and validate heading before constructing lateral input. + +```csharp +double startProjectionUpperS = request.PlanningScope == EmPlanningScope.FullDirectionSegment + ? Math.Min(segment.LengthMeters, configuration.Frenet.MaximumProjectionDistanceMeters) + : segment.LengthMeters; +if (!projector.TryProject(request.VehicleState.Pose, segment, 0d, startProjectionUpperS, + configuration.Frenet.MaximumProjectionDistanceMeters, 0d, out FrenetProjection startProjection) || + Math.Abs(startProjection.HeadingError) >= Math.PI / 2d) +{ + return Failure(EmPlanningStatus.ProjectionFailed, request, + "Vehicle pose is not an admissible start state for the selected direction segment."); +} +``` + +- [ ] **Step 4: Re-run `em-planning-service` and verify GREEN.** Existing Rolling projection behavior must remain green. + +### Task 2: Make cancellation terminal and non-publishable + +**Files:** +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` + +**Interfaces:** +- Produces: `Cancelled` results with null candidate/path/trajectory at every layer. + +- [ ] **Step 1: Write failing optimizer tests.** Use a solver that returns one valid candidate and cancels the supplied source before the next iteration. Assert both optimizers return `Cancelled`, not `SuccessWithFallback`, and expose no candidate. + +```csharp +Verification.Equal(EmPlanningStatus.Cancelled, result.Status, + "cancellation is never converted to fallback success"); +Verification.True(result.Path == null, "cancelled lateral result has no path"); +``` + +- [ ] **Step 2: Verify RED with the focused groups.** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-integration +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration +``` + +Expected: at least one optimizer currently returns `SuccessWithFallback`. + +- [ ] **Step 3: Implement minimal cancellation precedence.** Special-case cancellation in each `FallbackOrFailure`, and check the token after LS, after ST, after assembly, and immediately before service success publication. + +```csharp +if (failureStatus == EmPlanningStatus.Cancelled) + return Failed(EmPlanningStatus.Cancelled, failureReason); +``` + +- [ ] **Step 4: Re-run both optimizer groups and `em-planning-service`; verify GREEN.** + +### Task 3: Add shared linearized curvature hard constraints + +**Files:** +- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs` +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` + +**Interfaces:** +- Produces: `LateralCurvatureLinearization.Create(input, layout, iterate)` returning immutable station affines with indices, gradient, and constant. +- Consumers: objective terms and hard constraints use the exact same affine coefficients. + +- [ ] **Step 1: Write a failing QP-shape test.** For a straight reference and zero iterate, set vehicle maximum curvature to `0.25 1/m`; assert every station has a row equivalent to `-0.25 <= DDL(i) <= 0.25`. Also assert the total constraint count increases by the station count. + +```csharp +Verification.Equal(expectedOldRows + layout.StationCount, problem.ConstraintCount, + "one curvature hard-bound row is emitted per station"); +Verification.True(HasBound(problem, + new Dictionary { { layout.DDL(station), 1d } }, -0.25d, 0.25d), + "straight-path curvature affine is hard bounded"); +``` + +- [ ] **Step 2: Run `lateral-model` and verify RED.** + +- [ ] **Step 3: Extract the existing affine calculation without changing its formula.** Move `CreateCurvatureAffines` and its value type from `LateralObjectiveBuilder` to the new internal file. Keep the nonlinear formula in `LateralGeometryEvaluator`/independent validator unchanged. + +- [ ] **Step 4: Add one curvature row per station in `LateralConstraintBuilder`.** Bounds are `[-maximumVehicleCurvature, +maximumVehicleCurvature]` after subtracting the affine constant. + +```csharp +AddRow(constraints, lower, upper, ref row, + -maximumCurvature - affine.Constant, + maximumCurvature - affine.Constant, + affine.Indices, affine.Gradient); +``` + +- [ ] **Step 5: Update test solver problem classification so the added lateral rows are not mistaken for ST rows, then run `lateral-model`, `lateral-integration`, and `em-planning-service` GREEN.** + +### Task 4: Relinearize rejected solved vectors and report iteration exhaustion honestly + +**Files:** +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` + +**Interfaces:** +- Produces: rejected, parseable solver candidates may advance only the SQP iterate/warm start; they never replace `lastValidatedPath`. + +- [ ] **Step 1: Write failing tests.** Configure a low curvature limit so the first candidate is strict and the second parseable candidate fails nonlinear validation. Assert the third QP/warm start is based on the second candidate, while a later timeout still returns the first strict path. Change the outer-limit assertion from ordinary `Success` to `SuccessWithFallback` with a non-empty reason. + +- [ ] **Step 2: Run `lateral-integration` and verify RED.** + +- [ ] **Step 3: Move iterate/warm-start advancement to immediately after a parseable solved candidate, while updating `lastValidatedPath` only after independent validation.** Return `SuccessWithFallback` when the outer loop ends with a strict candidate but without satisfying convergence. + +- [ ] **Step 4: Run `lateral-integration` and `lateral-real-osqp` GREEN.** + +### Task 5: Share one LS/ST solver timeout budget + +**Files:** +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanner.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanner.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` + +**Interfaces:** +- Keeps: existing public `Plan(input, token)` and `Optimize(input, token)` entry points. +- Adds: internal overloads accepting a finite positive `TimeSpan solveBudget`. + +- [ ] **Step 1: Write a failing service test.** Delay a lateral fake solve by at least 100 ms under a 2 s configured timeout, record all `QpSolverSettings.MaximumSolveDuration` values, and assert the first ST call receives less than 1.95 s rather than a fresh 2 s. + +- [ ] **Step 2: Run `em-planning-service` and verify RED.** + +- [ ] **Step 3: Add internal explicit-budget overloads.** Default public overloads continue deriving budget from configuration; service starts one monotonic `Stopwatch` immediately before LS and passes `configuredBudget - elapsed` to LS and then ST. Non-positive remaining time returns `SolverTimedOut` with no trajectory. + +- [ ] **Step 4: Add a final elapsed/cancellation check before publication and run `lateral-integration`, `longitudinal-integration`, and `em-planning-service` GREEN.** + +### Task 6: Preserve reference S separately from optimized PathS + +**Files:** +- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` + +**Interfaces:** +- Extends internal `InterpolatedLateralPathPoint` with `ReferenceS`. +- Keeps public `EmTrajectoryPoint` shape unchanged. + +- [ ] **Step 1: Write a failing curved/offset-path assembly test.** Construct a validated lateral path where reference-S and chord PathS differ; assert each output point's `SegmentLocalS` is the interpolated reference-S and `PathS` remains the ST progress value. + +- [ ] **Step 2: Run `trajectory` and verify RED.** Current assembly writes `sample.PathS` into both fields. + +- [ ] **Step 3: Interpolate `ReferenceS` using the same PathS bracket and pass `geometry.ReferenceS` as `SegmentLocalS`.** Update publication bounds so segment-local S is checked against direction-segment length/reference bound, while PathS is checked against the optimized path upper bound. + +- [ ] **Step 4: Run `trajectory` and `em-core-all` GREEN.** + +### Task 7: Core regression and diff hygiene + +**Files:** +- Verify only; no broad formatting. + +- [ ] **Step 1: Run the complete EM verification set.** + +```powershell +dotnet build ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-restore +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-build -- em-all +``` + +- [ ] **Step 2: Check only scoped diffs.** + +```powershell +git diff --check -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost +git status --short -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost +``` + +Expected: all groups pass; no whitespace errors; `EmPlannerConfiguration.cs` remains exactly the user's pre-existing modification. diff --git a/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md b/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md new file mode 100644 index 0000000..7b6bf8b --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md @@ -0,0 +1,111 @@ +# TrapMap完整图片导出与终端日志开关设计 + +## 目标 + +为`MovementTest.Trapmaptest.cs`增加两个相互独立的测试开关: + +- 成功建图后,将完整栅格地图独立渲染为300 DPI PNG,不依赖Clumsy当前视口、缩放或其他Painter。 +- 控制TrapMap调试信息是否同步打印到宿主进程终端,同时始终保留`DLog`日志。 + +现有`TrapMapTest` Painter初始化和清理已经使用世界坐标调用`UI.GetPainter("TrapMapTest")`,本次不重复修改。两腿检测ROI继续使用车体局部Painter及`false`参数。 + +## 用户开关与默认值 + +在`TrapMapTest`手动编辑区增加: + +```csharp +private const bool _saveFullMapImage = true; +private const bool _enableTerminalDebugLog = true; +``` + +两个值传入`TrapMapBuilder`或共享日志/导出组件。关闭图片开关时不得创建目录、临时文件或PNG。关闭终端开关时只抑制`Console.WriteLine`,不得抑制`DLog`和必要的UI提示。 + +## 图片内容 + +图片从最终发布的`GridMapData`离屏渲染,包含完整地图边界而不是屏幕截图: + +- 白色背景。 +- 浅灰色完整栅格线,确保每个小格可见。 +- 红色占用栅格。 +- 蓝色车辆轮廓、几何中心和朝向。 +- 绿色工作站目标标记。 +- 黑色地图外边界。 +- 标题/图例区域,显示世界边界、分辨率、行列数、占据率、障碍物数量、轮胎层状态和输入来源。 + +世界X轴在图片中向右;世界Y轴向上,因此从`Cells[row,col]`映射到位图时反转图像Y方向。工作站仅绘制标记,不写入占用数据。 + +## 图片尺寸与文件约束 + +- 每个栅格使用`4×4`像素。 +- PNG水平和垂直DPI都设置为`300`。 +- 包含边距和标题后,任一图片边长不得超过`4000`像素。 +- 最终PNG文件大小不得超过`50 * 1024 * 1024`字节。 + +尺寸在分配RGBA像素缓冲区前检查。若超过4000像素,跳过导出并报告明确原因。编码先写入同目录临时文件,完成后检查实际字节数;超过50MB时删除临时文件,不留下超限最终文件。只有所有检查通过后,才原子移动/重命名为最终PNG。 + +典型`327×139`地图的栅格主体约为`1308×556`像素,另加标题和边距。 + +## 保存位置与命名 + +输出根目录使用宿主进程当前工作目录: + +```text +TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png +``` + +毫秒时间戳避免同一秒多次测试覆盖。目录只在图片开关打开且地图成功后创建。临时文件使用同目录、同文件名加`.tmp`后缀,以保证最终重命名不跨磁盘。 + +## 日志行为 + +引入TrapMap专用日志入口,其行为为: + +```text +所有消息 -> DLog.Log(message, "TrapMapTest") +终端开关开启 -> 额外Console.WriteLine("[TrapMapTest] " + message) +``` + +至少覆盖测试开始、输入参数、Detour位姿、车辆尺寸、地图边界/尺寸、轮胎层状态、图片保存成功/跳过/失败、最终统计和测试停止。图片错误不得因终端开关关闭而静默,仍必须进入`DLog`。 + +## 组件边界 + +图片导出放在独立文件`ClumsyPilot/TrapMapImageExporter.cs`,避免继续扩大已经较长的MovementTest文件。组件只消费不可变的导出请求数据:地图、车辆位姿、工作站、轮胎层元数据和目标文件路径;它不读取Detour、雷达或UI,也不修改栅格。 + +`MovementTest.Trapmaptest.cs`负责开关、调用时机、日志和错误降级。导出发生在地图成功生成之后;导出失败不改变`TrapMapBuilder.Succeeded`或`GridMap`。 + +实现使用内部纯C# RGBA光栅器绘制栅格、车辆、工作站和5×7位图文字,再由精确版本`StbImageWriteSharp` 1.16.7编码PNG。编码后立即在`IHDR`后插入`pHYs=11811/11811/unit1`,以保留300 DPI元数据。运行时不依赖平台绘图程序集或原生图形资产;除单个托管Stb编码程序集外,光栅、元数据和文件流程均只使用BCL。 + +## 错误处理 + +以下情况只导致图片导出失败,不导致建图失败: + +- 图片开关关闭。 +- 图片尺寸超过4000像素。 +- 输出目录创建失败。 +- RGBA缓冲区创建、绘制或PNG编码异常。 +- 临时文件超过50MB。 +- 临时文件重命名失败。 + +异常路径必须尽力删除本次临时文件,不得删除已有的成功PNG。 + +## 验证要求 + +至少验证: + +1. 图片开关关闭时不创建文件和目录。 +2. 小型已知栅格导出的PNG由BCL测试解码器重新读取;所有chunk CRC有效,`IHDR`为RGBA8,`pHYs`表示300 DPI,像素尺寸符合4像素/格及布局规则。 +3. PNG中占用格、车辆和工作站采样位置颜色正确,Y轴没有上下颠倒。 +4. 超过4000像素的请求在RGBA缓冲区分配前失败。 +5. 最终路径使用毫秒时间戳且不覆盖旧文件。 +6. 成功文件严格小于或等于50MB,超限临时文件被删除。 +7. 终端开关开启时消息同时进入DLog和终端;关闭时仍进入DLog但不写终端。 +8. 图片失败时地图仍为成功状态。 +9. 现有源码契约、栅格行为、生命周期测试及`ClumsyPilot`编译继续通过。 + +## 非目标 + +- 不截取Clumsy/CycleGUI窗口。 +- 不保存紫色UI背景、小车3D模型、绿色两腿ROI或橙色检测猜测线。 +- 不改变栅格数据格式或地图边界计算。 +- 不接入新的点云来源。 +- 不修改`TrajPlanner`。 +- 不提交或暂存本次工作区改动。 diff --git a/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md b/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md new file mode 100644 index 0000000..905228b --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-trap-map-pure-png-design.md @@ -0,0 +1,52 @@ +# TrapMap Managed PNG Export Design + +## Goal + +Replace `System.Drawing.Common` in TrapMap image export so Clumsy can save the full grid PNG without depending on platform-specific drawing assemblies. + +## Chosen approach + +Use a small internal RGBA rasterizer for grid primitives and `StbImageWriteSharp` 1.16.7 for PNG encoding. Keep the public request/result API and the existing TrapMap call site unchanged. + +Alternatives rejected: + +- Copying `System.Drawing.Common.dll` beside the build output is unreliable because Clumsy performs its own dependency attachment and can load the incompatible `netstandard2.0` facade first. +- `SkiaSharp` adds native Windows assets and more deployment points. +- `ImageSharp` has a larger dependency/licensing surface and its current release does not target this project's `netstandard2.0` runtime. + +`StbImageWriteSharp` is selected because its package contains one managed `netstandard2.0` implementation and declares no dependencies. It has no native assets or alternate platform facades for Clumsy to select incorrectly. + +## Rasterization + +- Allocate an in-memory 32-bit RGBA pixel buffer after the existing 4000-pixel edge validation. +- Preserve the current canvas dimensions, 4 pixels per cell, colors, Y inversion, vehicle/workstation geometry, header height, and 50 MiB final-file limit. +- Draw filled rectangles, 1/2-pixel lines, circles, crosses, and vehicle polygons with deterministic integer raster operations. +- Render header and workstation text with a small embedded ASCII bitmap font. Non-ASCII metadata is sanitized to a printable fallback for the PNG header only; original messages remain unchanged in DLog/Console. +- Keep header lines non-overlapping and include bounds, resolution, rows/columns, occupied count/rate, obstacle count, tire status/message, and input source. + +## PNG encoding + +- Pass the RGBA pixel buffer to `StbImageWriteSharp.ImageWriter.WritePng`. +- Insert a standard `pHYs` chunk immediately after `IHDR`, with X/Y both 11,811 pixels per metre and unit `1` (300 DPI). +- Calculate the inserted chunk's CRC-32 and write its integers in big-endian order. +- Validate the completed PNG before publication; encoding or metadata failures remain contained export failures. +- Preserve the current collision-safe temporary-file reservation, actual encoded-size check, atomic move, and best-effort cleanup behavior. + +## Project and deployment changes + +- Remove the `System.Drawing.Common` package reference and `DeployFrameworkDrawingRuntime` target from `ClumsyPilot.csproj`. +- Add `StbImageWriteSharp` version 1.16.7. No other new package is allowed. +- The final build output must not require or deploy `System.Drawing.Common.dll` for TrapMap; it may deploy the single managed `StbImageWriteSharp.dll`. +- Existing Clumsy references are not modified. + +## Verification + +- TDD first proves the current exporter/package still depends on `System.Drawing.Common`. +- Reflection/source contracts assert the old package, build target, `using System.Drawing`, and drawing types are absent, and the exact Stb package is present. +- Decode the generated PNG in the test with a test-only decoder or framework reader and verify dimensions, 300 DPI metadata, representative colors, Y inversion, header separation, unique concurrent filenames, and no temporary files. +- Run source, build, grid, lifecycle, and image tests; require zero build errors and no new warnings. + +## Non-goals + +- No changes to grid construction, obstacle/tire inputs, vehicle motion, Painter visualization, export switches, output directory, or file-size/pixel limits. +- No screenshot capture and no new general-purpose graphics framework. diff --git a/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md b/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md index a6ccca9..ee0d19b 100644 --- a/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md +++ b/docs/superpowers/specs/2026-07-26-hybrid-astar-coarse-path-design.md @@ -4,92 +4,126 @@ 在现有栅格地图能力之上,实现四舵轮 AMR 的 Hybrid A* 空间粗路径:支持前进、倒车、换向、静态/投影障碍绕行、车体碰撞检查、可配置的终点位置与航向容差,以及稠密路径与方向分段输出。 -本阶段**不包含**曲线平滑、B 样条、Bezier、局部 QP、SQP、Reeds-Shepp 精确终点连接、速度/加速度/时间参数化或底盘舵角控制。它们由后续独立模块处理。 +本阶段**不包含**曲线平滑、B 样条、Bezier、局部 QP、SQP、Reeds-Shepp 精确终点连接、速度/加速度/时间参数化或底盘舵角控制。第一版采用汽车式恒曲率模型,不支持蟹行、纯横移和原地旋转。它们由后续独立模块处理。 ## 已确认的约束 - 地图与外部人工障碍输入继续使用现有世界坐标单位:mm。 - Hybrid A* 内部统一使用 m、rad、1/m;单位转换只能经过地图适配与 `Utils`,不能散落在搜索代码中。 - 人工障碍物第一版支持以几何中心放置的轴对齐矩形和圆形。 -- `TwoLegDetect` 是可选障碍物输入,不是规划地图是否可用的唯一条件;它的检测结果经投影后与人工障碍物合并。 +- `TwoLegProjectionInput` 是可选障碍物输入,不是规划地图是否可用的唯一条件;上层采集到的 `TwoLegDetect` 结果经 DTO 投影后与人工障碍物合并。 - 环境占据地图只保存外部障碍物,绝不写入 AMR 自身足迹。AMR 尺寸、当前位姿和安全余量只用于粗路径的车体碰撞检测。 - 障碍物不做安全膨胀;安全余量仅通过碰撞检查时的扩大车辆矩形应用,防止双重膨胀。 - 第一版的终点条件为可配置的位置容差和车头航向容差。默认建议为 0.15 m 与 5°,而非精确连接到目标位姿。 +- `PrimitiveLength=0.50 m` 是单条原语的最大长度,不是终点只能出现的离散间隔。每条原语必须在内部积分点检查目标条件;该原语内首次满足条件时立即截断并建立终点候选节点。终点候选仍须进入 Open List,只有当它作为当前最优有效节点出队时,整个搜索才返回成功。 +- 碰撞检测必须保守:不得因航向离散、车辆中心的亚栅格偏移、距离场误差或原语离散采样而发布可能碰撞的路径。 +- `ObstacleDistanceField` 只能作为保守快速放行和代价估计,任何可能高估真实净空的近似都不得用于跳过精确车体碰撞检查。 - 测试是交付的一部分:纯逻辑契约测试与参照 `Map` 的 `MovementTest` 集成/可视化入口都必须提供。 - 每个文件只承担一个明确职责;对外调用通过模块门面类完成,不让调用者拼装搜索、地图和碰撞的内部对象。 +- 推荐调用方只使用 `CoarsePathPlanningService` 一次完成建图、粗路径搜索和可选调试发布;`PlanningMapFactory` 与 `HybridAStarPlanner` 是可独立测试、复用的下层模块门面。请求、结果、枚举、障碍物 DTO 和值对象仍然是可公开构造的数据契约。 ## 目录和命名空间 ```text ClumsyPilot/ParkrobTrajplanner/ -├── Initial_plan/ # 已有路线与方案文档,不放运行时代码 -├── Utils/ # MultiWheelC.TrajectoryPlanning.Utils -│ ├── AngleMath.cs -│ ├── UnitConverter.cs -│ ├── CoordinateTransform.cs -│ ├── NumericGuard.cs -│ └── GridIndex.cs -├── Map/ # MultiWheelC.TrajectoryPlanning.Mapping +├── Initial_plan/ ------ 已有路线与方案文档,不放运行时代码 +├── Utils/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.Utils +│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引 +│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率单位转换 +│ ├── CoordinateTransform.cs ------ 车体坐标系与世界坐标系二维刚体变换 +│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验 +│ └── GridIndex.cs ------ 不可变行列索引值对象 +├── Map/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.Mapping │ ├── Core/ -│ │ ├── EnvironmentGridMap.cs -│ │ ├── MapBuildRequest.cs -│ │ ├── EnvironmentMapBuildResult.cs -│ │ └── EnvironmentMapBuilder.cs +│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 单位占据栅格 +│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界值对象 +│ │ ├── MapBuildRequest.cs ------ 环境地图边界、分辨率和障碍物图层输入 +│ │ ├── EnvironmentMapBuildResult.cs ------ 环境地图构建状态、来源摘要和失败原因 +│ │ └── EnvironmentMapBuilder.cs ------ 校验并合并人工与投影障碍物图层 │ ├── Obstacles/ -│ │ ├── IMapObstacle.cs -│ │ ├── AxisAlignedRectangleObstacle.cs -│ │ ├── CircleObstacle.cs -│ │ └── MapObstacleRasterizer.cs +│ │ ├── IMapObstacle.cs ------ 人工和投影障碍物的公共几何契约 +│ │ ├── AxisAlignedRectangleObstacle.cs ------ 世界轴对齐矩形障碍物 DTO +│ │ ├── CircleObstacle.cs ------ 世界坐标圆形障碍物 DTO +│ │ └── MapObstacleRasterizer.cs ------ 通过形状与格子相交测试写入占据栅格 │ ├── Sources/ -│ │ ├── ManualObstacleSource.cs -│ │ └── TwoLegObstacleProjector.cs +│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一投影接口 +│ │ ├── ObstacleSourceStatus.cs ------ Applied、Empty、Unavailable、Invalid 来源状态 +│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和世界障碍物集合 +│ │ ├── ManualObstacleSource.cs ------ 把人工圆和矩形作为世界障碍物输出 +│ │ ├── TwoLegProjectionInput.cs ------ 已验证两腿端点、检测状态和检测时车辆位姿 DTO +│ │ ├── TwoLegObstacleSource.cs ------ 把 TwoLeg 快照接入统一障碍来源接口 +│ │ └── TwoLegObstacleProjector.cs ------ 把车体系两腿端点投影为世界坐标圆障碍 │ ├── Planning/ -│ │ ├── PlanningGridMap.cs -│ │ ├── ObstacleDistanceField.cs -│ │ └── PlanningMapAdapter.cs -│ ├── PlanningMapRequest.cs -│ ├── PlanningMapBuildResult.cs -│ └── PlanningMapFactory.cs +│ │ ├── PlanningGridMap.cs ------ 只读 m 单位占据图、距离场和规划可用状态 +│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间生成栅格中心精确欧氏距离 +│ │ ├── ObstacleDistanceField.cs ------ 生成不高估障碍净空的保守欧氏距离下界 +│ │ ├── PlanningMapCache.cs ------ 线程安全的输入指纹与占据哈希快照缓存 +│ │ └── PlanningMapAdapter.cs ------ 深拷贝占据数据并完成 mm 到 m 的边界适配 +│ ├── PlanningMapRequest.cs ------ 地图门面的统一输入契约 +│ ├── PlanningMapBuildResult.cs ------ 地图门面的统一输出契约 +│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共行为入口 │ └── Test/ -│ └── MovementTest.MapTest.cs -├── CoarsePath/ # MultiWheelC.TrajectoryPlanning.CoarsePath +│ ├── MovementTest.MapTest.cs ------ Clumsy UI 地图构建与可视化测试入口 +│ └── Visualization/ +│ ├── PlanningMapImageExportRequest.cs ------ 规划快照、叠加层和输出选项 DTO +│ ├── PlanningMapImageExportResult.cs ------ PNG 导出状态、路径、大小和诊断 +│ ├── PlanningMapImageExporter.cs ------ 校验请求、编排渲染并原子发布 PNG +│ ├── PlanningMapImageRenderer.cs ------ 把地图、起终点、车体和路径绘制到 RGBA +│ └── ValidatedPngWriter.cs ------ Stb PNG 编码、结构和 CRC 完整性校验 +├── CoarsePath/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.CoarsePath │ ├── Contracts/ -│ │ ├── Pose2D.cs -│ │ ├── VehicleParameters.cs -│ │ ├── PlanningRequest.cs -│ │ ├── HybridAStarConfiguration.cs -│ │ ├── PlanningResult.cs -│ │ ├── PlanningStatus.cs -│ │ ├── CoarsePathPoint.cs -│ │ └── PathSegment.cs +│ │ ├── Pose2D.cs ------ m/rad 单位的不可变二维位姿 +│ │ ├── TravelDirection.cs ------ Forward 与 Reverse 运动方向枚举 +│ │ ├── GoalDirectionConstraint.cs ------ Any、Forward、Reverse 目标进入方向约束 +│ │ ├── VehicleParameters.cs ------ 车体尺寸、安全余量和最大曲率参数 +│ │ ├── PlanningRequest.cs ------ 地图、起终点、起始曲率和方向约束 +│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差配置 +│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和方向分段 +│ │ ├── PlanningStatus.cs ------ 输入、碰撞、搜索和验证结果枚举 +│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计 +│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和保守净空 +│ │ ├── CoarsePathPointSource.cs ------ 起点、普通原语和终点截断来源枚举 +│ │ └── PathSegment.cs ------ 前进/倒车分段及其包含式索引范围 │ ├── Vehicle/ -│ │ ├── VehicleKinematics.cs -│ │ ├── HeadingFootprintTemplate.cs -│ │ ├── HeadingFootprintTemplateCache.cs -│ │ └── FootprintCollisionChecker.cs +│ │ ├── VehicleKinematics.cs ------ 解析并校验车辆保守最大曲率 +│ │ ├── VehicleFootprint.cs ------ 计算扩大车体矩形、包围盒和外接圆 +│ │ ├── OrientedRectangleCellIntersection.cs ------ 精确判断旋转车体矩形与栅格矩形相交 +│ │ └── FootprintCollisionChecker.cs ------ 边界、距离场、精确和扫掠碰撞检查 │ ├── Search/ -│ │ ├── MotionPrimitive.cs -│ │ ├── MotionPrimitiveGenerator.cs -│ │ ├── HybridAStarNode.cs -│ │ ├── HybridAStarNodeKey.cs -│ │ ├── GoalToleranceChecker.cs -│ │ ├── GridDijkstraHeuristic.cs -│ │ └── HybridAStarSearch.cs +│ │ ├── MotionPrimitive.cs ------ 单条恒曲率原语及其实际截断长度描述 +│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分前进/倒车原语并保留内部点 +│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 兼容且确定性排序的 Open List +│ │ ├── SearchCostCalculator.cs ------ 统一计算长度、倒车、换向、曲率和净空代价 +│ │ ├── HybridAStarNode.cs ------ 连续位姿、代价、父索引和原语描述 +│ │ ├── HybridAStarNodeKey.cs ------ 位置格、航向格、方向和曲率离散键 +│ │ ├── GoalToleranceChecker.cs ------ 位置、航向和目标进入方向容差判断 +│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域二维绕障距离启发 +│ │ └── HybridAStarSearch.cs ------ 节点扩展、重开、限额和终点候选管理 │ ├── Output/ -│ │ ├── PathBacktracker.cs -│ │ ├── CoarsePathAssembler.cs -│ │ └── CoarsePathValidator.cs -│ ├── HybridAStarPlanner.cs +│ │ ├── PathBacktracker.cs ------ 按父索引确定性重建原语内部点 +│ │ ├── CoarsePathAssembler.cs ------ 生成弧长、换向点和包含式方向分段 +│ │ └── CoarsePathValidator.cs ------ 复核数值、碰撞、曲率、终点和分段 +│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的纯搜索下层门面 +│ ├── Facade/ +│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需地图、起终点、车辆和调试选项 +│ │ ├── CoarsePathPlanningJobResult.cs ------ 同时返回地图构建结果和粗路径结果 +│ │ ├── PlanningDebugOptions.cs ------ 地图、路径和碰撞调试发布开关 +│ │ ├── IPlanningDebugSink.cs ------ 不影响规划状态的调试结果消费接口 +│ │ └── CoarsePathPlanningService.cs ------ 建图、缓存、搜索和调试编排的一次调用入口 │ └── Test/ -│ ├── CoarsePathScenarioFactory.cs -│ └── MovementTest.CoarsePathTest.cs -└── README.md # 只说明模块边界、调用入口与文档链接 +│ ├── CoarsePathScenarioFactory.cs ------ 生成固定、可复现的地图和规划场景 +│ └── MovementTest.CoarsePathTest.cs ------ 后台运行规划并在 Clumsy UI 绘制结果 +│ └── README.md ------ 粗规划模块边界、公共调用入口和文档链接 ClumsyPilot/tests/ -├── verify_planning_utils.ps1 -├── verify_planning_map_adapter.ps1 -├── verify_coarse_path_search.ps1 -└── verify_coarse_path_integration.ps1 +├── verify_planning_utils.ps1 ------ 单位、角度、坐标和数值守卫测试 +├── verify_planning_map_factory.ps1 ------ 地图输入、图层事务、快照和版本测试 +├── verify_planning_map_adapter.ps1 ------ 地图栅格化、图层、适配和距离场测试 +├── verify_planning_map_image.ps1 ------ 只读快照 PNG 渲染、限制和原子发布测试 +├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞测试 +├── verify_coarse_path_search.ps1 ------ 原语截断、搜索、方向、限额和重开测试 +├── verify_coarse_path_integration.ps1 ------ Map 到最终路径验证的端到端测试 +└── benchmark_coarse_path.ps1 ------ 参考与压力场景性能资源验收 ``` `Occupancygird_Map/Map_test` 是当前原型位置。实施时会把其中仍然需要的地图能力按以上职责迁移到 `Map`,避免继续向两个现有大文件叠加功能;图片导出可以保留为独立地图测试辅助,不成为规划器依赖。 @@ -110,7 +144,7 @@ ClumsyPilot/tests/ ### 对外调用门面 -粗路径模块不直接实例化 `EnvironmentMapBuilder`、`MapObstacleRasterizer`、`TwoLegObstacleProjector` 或 `PlanningMapAdapter`。`PlanningMapFactory` 是 `Map` 模块唯一的公共创建入口: +`HybridAStarPlanner` 不直接实例化任何地图对象;`CoarsePathPlanningService` 只持有 `PlanningMapFactory`,不接触 `EnvironmentMapBuilder`、`MapObstacleRasterizer`、来源投影器或 `PlanningMapAdapter`。`PlanningMapFactory` 是 `Map` 模块唯一的公共创建入口: ```csharp public sealed class PlanningMapFactory @@ -119,7 +153,7 @@ public sealed class PlanningMapFactory } ``` -粗路径调用只依赖其稳定输出: +下层模块独立调用时只依赖其稳定输出: ```csharp var mapResult = new PlanningMapFactory().Create(mapRequest); @@ -136,13 +170,44 @@ var result = new HybridAStarPlanner().Plan(new PlanningRequest }); ``` -`PlanningMapRequest` 集中地图边界/分辨率、人工障碍物、可选 `TwoLegDetect` 投影输入和空旷地图声明;`PlanningMapBuildResult` 返回 `Succeeded`、失败原因、每个障碍物来源的摘要,以及成功时不可变的 `PlanningGridMap`。因此 A* 的 `PlanningRequest.Map` 始终是已完成校验、mm→m 适配、距离场生成的输入,规划器不需要了解地图构造细节。 +`PlanningMapRequest` 集中地图边界/分辨率、`IReadOnlyList` 和空旷地图声明;`PlanningMapBuildResult` 返回 `Succeeded`、失败原因、每个障碍物来源的摘要、缓存命中类型,以及成功时不可变的 `PlanningGridMap`。因此 A* 的 `PlanningRequest.Map` 始终是已完成校验、投影、栅格化、mm→m 适配和距离场生成的输入,规划器不需要了解地图构造细节。 + +### 统一障碍物来源与投影 + +所有人工、TwoLeg 和后续障碍物输入统一实现: + +```csharp +public interface IMapObstacleSource +{ + string SourceId { get; } + long SourceVersion { get; } + bool IsRequired { get; } + ObstacleProjectionResult ProjectToWorld(); +} +``` + +`ProjectToWorld` 只能消费构造来源对象时已经取得的不可变快照,不得在内部读取传感器、定位、UI 或系统时间。它统一返回世界坐标 mm 几何体: + +```text +ObstacleProjectionResult: + SourceId + SourceVersion + Status Applied/Empty/Unavailable/Invalid + Message + IReadOnlyList Obstacles +``` + +必需来源返回 `Unavailable/Invalid` 时地图构建失败;可选来源返回上述状态时记录诊断并继续处理其他来源。多个来源产生重叠障碍物是合法的,占据写入具有幂等语义。 + +`ManualObstacleSource` 直接输出已经位于世界坐标系的圆和轴对齐矩形。`TwoLegProjectionInput` 是纯数据 DTO,明确包含检测状态、车体坐标系中的两个端点、端点半径、检测时刻的 AMR 世界位姿,以及 mm/deg 单位声明。`MovementTest` 或上层采集适配器负责调用现有 `TwoLegDetect`,随后把结果封装为快照;`TwoLegObstacleSource` 委托 `TwoLegObstacleProjector` 执行确定性车体到世界变换并输出零个或两个 `CircleObstacle`。 + +新增障碍物来源只需投影为现有 `IMapObstacle` 几何体;如果需要新增多边形等几何类型,必须同时为 `MapObstacleRasterizer` 添加保守的形状-栅格相交实现和成功/边界/失败测试。任何来源都不得应用车辆安全余量。 ### 环境地图与图层 `EnvironmentGridMap` 保存 mm 单位的地图边界、分辨率和仅含外部障碍物的占据单元。它不接受 `MarkVehicleFootprint` 一类接口。 -`EnvironmentMapBuilder` 是地图模块的唯一对外构造入口: +`EnvironmentMapBuilder` 是由 `PlanningMapFactory` 使用的内部环境图构造器: ```csharp public sealed class EnvironmentMapBuilder @@ -151,11 +216,11 @@ public sealed class EnvironmentMapBuilder } ``` -它依次校验地图参数、栅格化人工障碍物、投影可用的 `TwoLegDetect` 结果、合并占据单元,并报告每个来源是否生效。任何可选传感器输入失败都不会删除已成功构建的人工地图。 +它依次校验地图参数、按 `SourceId` 确定性排序来源、收集 `ObstacleProjectionResult`、合并所有成功投影的 `IMapObstacle` 并调用唯一栅格化器。任何可选来源失败都不会删除其他来源已成功构建的占据内容。 人工障碍物实现 `IMapObstacle`。`AxisAlignedRectangleObstacle` 使用 `(CenterXmm, CenterYmm, WidthMm, HeightMm)`;`CircleObstacle` 使用 `(CenterXmm, CenterYmm, RadiusMm)`。两者的尺寸必须为有限正数,矩形轴与世界 X/Y 轴对齐。`MapObstacleRasterizer` 是唯一直接写入环境栅格的类。 -`TwoLegObstacleProjector` 仅负责将检测坐标通过现有车体到世界系变换变成 `CircleObstacle`,不负责地图边界、栅格化或规划可用性判断。 +`MapObstacleRasterizer` 是唯一直接写入 `EnvironmentGridMap` 的类型;各来源和投影器都不能取得地图写入接口。`TwoLegObstacleProjector` 不负责地图边界、栅格化、缓存或规划可用性判断。 ### 规划适配 @@ -168,47 +233,268 @@ public sealed class PlanningMapAdapter } ``` -适配时验证边界和分辨率、深拷贝占据数据、将长度从 mm 转为 m、将边界外固定解释为占据,并生成 `ObstacleDistanceField`。`PlanningGridMap` 是不可变的规划输入,包含 m 单位边界、分辨率、行列、占据数据、距离场、源地图版本与 `PlanningReady/PlanningBlockReason`。 +适配时验证边界和分辨率、深拷贝占据数据、将长度从 mm 转为 m、将边界外固定解释为占据,并生成 `ObstacleDistanceField`。`PlanningGridMap` 是不可变的规划输入,包含 m 单位边界、分辨率、行列、占据数据、距离场、来源版本摘要、快照标识与 `PlanningReady/PlanningBlockReason`。 -`PlanningReady` 的规则:有效的人工图层即可使地图可用于测试/规划;若没有人工障碍也没有 `TwoLegDetect`,在已明确配置“空旷地图”时仍可规划;未明确空旷语义的未观测区域则以 `PlanningBlockReason` 阻止规划,而不是暗中当作空闲。 +距离场使用精确的二维欧氏距离变换计算栅格中心到最近占据栅格中心的距离,再减去一个完整栅格对角线 `sqrt(2) * ResolutionMeters` 并截断到零,得到当前自由栅格内任意点到任意占据栅格矩形的保守下界。查询不得进行会抬高结果的插值;查询点使用其所在栅格的保守值。显式空旷地图的障碍物距离可以是正无穷,但车体边界检查仍必须先执行,地图外始终按占据处理。 + +距离场的用途受以下规则约束: + +- 当保守距离严格大于扩大车体外接圆半径时,碰撞检查器可以快速放行。 +- 当保守距离小于或等于外接圆半径时,必须执行精确的扩大车体矩形与占据栅格矩形相交检查。 +- `BodyClearance` 发布 `max(0, ConservativeCenterClearance - ExpandedFootprintCircumscribedRadius)`,作为车体净空的保守下界,不得声称为精确几何净空。 +- 多源距离场实现、空图语义、地图边界和最大栅格数都必须有自动化测试。 + +`PlanningReady` 的规则:至少一个成功来源提供有效障碍语义即可使地图用于测试/规划;所有来源均为空时,只有已明确配置 `AllowExplicitEmptyMap=true` 才可规划。必需来源失败或未明确空旷语义时,以 `PlanningBlockReason` 阻止规划,而不是暗中把未观测区域当作空闲。 + +### 现有地图优化与规划适配 + +本阶段不在旧 `GridMapData` 外再包一层长期兼容适配器,而是把其中经过测试的几何规则迁移为纯逻辑、静态快照式地图管线。迁移目标是消除 UI、传感器、车辆自身足迹和规划查询之间的职责耦合,同时降低 Hybrid A* 高频占据查询与距离查询的开销。 + +#### 现有职责拆分 + +| 现有类型/函数 | 处理方式 | 新职责位置 | +| --- | --- | --- | +| `TrapMapBounds.TryCreate` | 保留有限值、退化边界、分辨率和最大栅格数校验;移除“车辆到工作站”业务假设 | `MapBoundsMm`、`MapBuildRequest` | +| `GridMapData.WorldToGrid/GridToWorld` | 保留 X→列、Y→行和 XMax/YMax 排他规则;统一处理最后一个非完整栅格 | `EnvironmentGridMap`、`PlanningGridMap` | +| `GridMapData.Cells byte[,]` | 改为私有行优先 `byte[]`,索引固定为 `row * Cols + col`;不暴露可写数组 | 两类 GridMap 的内部存储 | +| `GridMapData.MarkObstacle/MarkObstacles` | 移除默认安全距离;保留候选包围盒裁剪和形状-格矩形相交 | `MapObstacleRasterizer` | +| `GridMapData.MarkVehicleFootprint` | 从地图模块删除,不提供兼容开关 | `FootprintCollisionChecker` 查询时处理 | +| `GridMapData.Copy` | 不再暴露可变副本;适配时只进行一次占据缓冲区深拷贝 | `PlanningMapAdapter` | +| `TrapMapLayerComposer.Compose` | 泛化为“可选来源失败不破坏其他成功来源”的事务语义 | `IMapObstacleSource`、`EnvironmentMapBuilder` | +| `TrapMapBuilder.Get` | 拆除 `MovementDefinition`、定位读取、TwoLeg 调用、Toast、Painter 和协程依赖 | 上层采集适配器 + `CoarsePathPlanningService` | +| `TrapMapImageExporter.cs` | 保留经过验证的纯托管 RGBA/PNG 能力,拆分请求、结果、渲染、发布和 PNG 校验职责;输入改为只读规划快照 | `Map/Test/Visualization` | + +#### 统一地图输入与静态快照 + +`PlanningMapRequest` 必须显式包含: + +```text +MapBoundsMm Bounds +float ResolutionMm +IReadOnlyList ObstacleSources +bool AllowExplicitEmptyMap +``` + +`Bounds` 使用 `[XMin, XMax) × [YMin, YMax)`;`ResolutionMm` 必须是 20~200 mm 的有限正数。来源 `SourceId` 必须非空且在一次请求内唯一,`SourceVersion` 必须非负,并在对应来源快照内容变化时递增。 + +`PlanningMapFactory.Create` 返回与来源输入隔离的不可变静态快照,不实现增量栅格更新或距离场局部修补。`PlanningGridMap` 保存各来源版本摘要、`InputFingerprint`、`OccupancyHash` 和通过 `Interlocked.Increment` 生成的进程内单调 `SnapshotId`。该计数器只标识快照,不保存地图内容。规划开始后只读取同一快照;上层即使收到新障碍物,也不得修改正在使用的占据缓冲区。 + +#### 两级指纹与快照复用 + +`PlanningMapCache` 是 `PlanningMapFactory` 的线程安全、容量为 4 的最近使用缓存;长期存在的 `CoarsePathPlanningService` 持有同一个工厂实例,因此多次规划可以复用快照。 + +每次创建按以下顺序判断: + +1. 调用纯快照来源的 `ProjectToWorld`,按 `SourceId` 排序,并对边界、分辨率、空图策略、来源状态/版本和规范化世界几何体计算 `InputFingerprint`。 +2. 若缓存中存在相同 `InputFingerprint`,直接返回同一不可变 `PlanningGridMap`;不重新栅格化或生成距离场。 +3. 输入指纹不同时重新栅格化,并对最终连续占据 `byte[]` 计算 `OccupancyHash`。 +4. 若地图几何参数和 `OccupancyHash` 与缓存快照相同,复用占据缓冲区与距离场,只生成包含新来源摘要和新 `SnapshotId` 的轻量快照。 +5. `OccupancyHash` 不同时才重新执行距离场变换并缓存完整新快照。 + +浮点几何按其 IEEE 位模式和固定字段顺序计算确定性指纹,不通过简单的“坐标除以分辨率取整”判断变化,避免圆或矩形在格边附近发生漏失效。缓存项同时保留规范化输入描述;`InputFingerprint` 命中后仍执行结构相等比较。`OccupancyHash` 命中后仍比较地图几何参数和连续占据缓冲区长度/内容,不能只依赖哈希值判等。 + +起点、终点、车辆尺寸、安全余量、Hybrid A* 参数和可视化开关不属于地图指纹。TwoLeg 的检测状态从有效变为无检测/不可用/过期时,其来源结果必须改变;上层采集适配器负责根据检测有效期构造正确的 `TwoLegProjectionInput`,地图来源接口本身不读取系统时间。 + +#### 存储、坐标和查询优化 + +- 占据数据使用私有连续 `byte[]`,避免公开 `byte[,]` 带来的可变性和多维数组索引开销。 +- `IsOccupied(row,col)`、`IsOccupiedWorld(x,y)` 和保守距离查询保持无分配、常数复杂度;地图外直接返回占据或零净空。 +- 世界坐标到格索引使用 `floor((value - min) / resolution)`;`XMax`、`YMax` 排他。最后一个格子的几何上界必须裁剪到实际地图上界。 +- 构造阶段使用 `checked` 计算 `Rows * Cols`,继续采用 4,000,000 格绝对上限;任何溢出或超限在分配前返回失败结果。 +- 圆形与矩形栅格化只遍历其裁剪后的格索引包围盒,不扫描全图。与地图完全不相交的合法障碍物被忽略并记录在来源摘要中,而不是使地图构建失败。 +- `EnvironmentGridMap` 只有程序集内部的占据写入入口;`PlanningGridMap` 不提供任何写入入口,也不返回内部缓冲区引用。 + +#### 距离场优化 + +`EuclideanDistanceTransform` 使用两次一维平方距离变换完成精确二维栅格中心距离计算,时间复杂度为 `O(Rows × Cols)`,不得为每个自由格遍历全部障碍格。中间数组按行列最大长度复用,最终距离使用连续 `double[]` 保存。 + +`ObstacleDistanceField` 在精确中心距离上执行前述保守修正并封装查询,不允许调用方直接取得未经修正的中心距离用于碰撞放行。显式空图不运行无意义的变换,直接构造正无穷障碍距离场;地图边界仍由规划地图和车体碰撞检查独立约束。 + +#### 地图迁移顺序 + +1. 先建立 `MapBoundsMm`、新占据存储和纯栅格化测试,不修改旧 UI 入口。 +2. 建立 `PlanningMapFactory`、静态快照、距离场和规划查询测试。 +3. 将人工障碍、TwoLeg DTO 与现有 Ghost/固定场景迁移为统一 `IMapObstacleSource`。 +4. 建立两级指纹缓存与 `CoarsePathPlanningService` 一次调用入口。 +5. 将旧 `TrapMapImageExporter.cs` 拆分为 `Map/Test/Visualization` 下的五个文件,并将 PNG 导出与 `MovementTest.MapTest` 改为只消费新快照。 +6. 新旧地图回归结果一致后,退役旧地图构建、车体写入和图层合成入口,并更新或删除对应旧反射测试。 + +迁移期间不得让 Hybrid A* 同时支持新旧两种地图类型;规划器从第一天起只接受 `PlanningGridMap`。 ## CoarsePath 模块 -### 对外门面与契约 +### 一次调用编排门面 -调用方只调用 `HybridAStarPlanner`: +常规调用方只调用: + +```csharp +public sealed class CoarsePathPlanningService +{ + public CoarsePathPlanningJobResult Plan( + CoarsePathPlanningJob job, + CancellationToken cancellationToken = default); +} +``` + +`CoarsePathPlanningJob` 集中以下输入: + +```text +PlanningMapRequest MapRequest +Pose2D Start +Pose2D Goal +VehicleParameters Vehicle +HybridAStarConfiguration Configuration +double StartVehicleCurvature +TravelDirection? StartDirection +GoalDirectionConstraint GoalDirection +PlanningDebugOptions Debug +``` + +推荐调用形式: + +```csharp +var result = planningService.Plan(new CoarsePathPlanningJob +{ + MapRequest = new PlanningMapRequest + { + Bounds = bounds, + ResolutionMm = 50f, + ObstacleSources = new IMapObstacleSource[] + { + new ManualObstacleSource(manualObstacles), + new TwoLegObstacleSource(twoLegSnapshot), + }, + AllowExplicitEmptyMap = true, + }, + Start = startPose, + Goal = goalPose, + Vehicle = vehicle, + Configuration = configuration, + Debug = new PlanningDebugOptions + { + VisualizeMap = true, + VisualizePath = true, + }, +}, cancellationToken); +``` + +一次调用内部固定执行: + +```text +PlanningMapFactory.Create(MapRequest) + → 地图失败则生成 FromMapFailure 结果 + → HybridAStarPlanner.Plan(PlanningRequest, cancellationToken) + → IPlanningDebugSink 按 Debug 开关发布地图、路径和诊断 +``` + +`CoarsePathPlanningJobResult` 同时保留 `PlanningMapBuildResult MapResult` 与 `PlanningResult PlanningResult`,使调用方能够取得实际使用的 `PlanningGridMap` 快照、缓存命中情况和粗路径状态。地图失败时不启动搜索;调试发布失败只写入调试诊断,不改变地图或路径规划状态。 + +`PlanningDebugOptions` 只包含 `VisualizeMap`、`VisualizePath`、`VisualizeCollisionChecks` 等旁路开关。`IPlanningDebugSink` 由 Clumsy `MovementTest` 适配实现;核心服务默认使用空实现,因此无 UI 环境与自动化测试不加载 Painter。 + +### 纯搜索下层门面 + +需要复用已有地图快照或单独测试搜索时调用 `HybridAStarPlanner`: ```csharp public sealed class HybridAStarPlanner { - public PlanningResult Plan(PlanningRequest request); + public PlanningResult Plan( + PlanningRequest request, + CancellationToken cancellationToken = default); } ``` -`PlanningRequest` 组合 `PlanningGridMap`、起点 `Pose2D`、目标 `Pose2D`、`VehicleParameters` 与 `HybridAStarConfiguration`。它不接受地图构建器、UI 对象或传感器对象。 +`PlanningRequest` 组合以下不可变输入: -`HybridAStarConfiguration` 集中所有可调参数,包括最大节点数、超时、航向分辨率、原语长度、积分步长、曲率等级、代价权重、是否允许倒车、位置容差和航向容差。初始值遵循已有技术方案:0.50 m 原语、0.05 m 积分、5° 航向离散、五级曲率、0.15 m 位置容差、5° 航向容差。 +- `PlanningGridMap Map` +- `Pose2D Start` 与 `Pose2D Goal` +- `VehicleParameters Vehicle` +- `HybridAStarConfiguration Configuration` +- `double StartVehicleCurvature`,未提供时显式使用零曲率 +- `TravelDirection? StartDirection`,`null` 表示起步方向不受约束 +- `GoalDirectionConstraint GoalDirection`,取值为 `Any`、`Forward` 或 `Reverse` -`PlanningResult` 始终返回明确 `PlanningStatus`、诊断信息、零或一条 `IReadOnlyList` 和 `IReadOnlyList`。粗路径不包含时间、速度、加速度、舵轮角或轮速。 +它不接受地图构建器、UI 对象或传感器对象。起点曲率必须在车辆最大曲率内,并离散到最近的合法曲率等级;该索引作为起始搜索状态的一部分。 + +`VehicleParameters` 明确使用车辆几何中心为 `Pose2D` 参考点,并包含 `LengthMeters`、`WidthMeters`、`SafetyMarginMeters`、可选 `MaximumCurvaturePerMeter` 与可选 `MinimumTurningRadiusMeters`。最大曲率和最小转弯半径同时存在时使用更保守的限制;两者都未提供时请求无效。 + +`HybridAStarConfiguration` 集中所有可调参数,包括最大节点数、超时、航向分辨率、原语最大长度、积分步长、碰撞采样步长、曲率等级、代价权重、是否允许倒车、位置容差和航向容差。默认值为: + +| 参数 | 默认值 | +| --- | --- | +| `PrimitiveLengthMeters` | 0.50 m,表示最大长度 | +| `IntegrationStepMeters` | 0.05 m | +| `MaximumCollisionCheckStepMeters` | 0.025 m,且运行时不得大于 `Map.ResolutionMeters / 2` | +| `HeadingResolutionRadians` | 5° | +| `CurvatureLevelCount` | 5 | +| `GoalPositionToleranceMeters` | 0.15 m | +| `GoalHeadingToleranceRadians` | 5° | +| `MaximumExpandedNodes` | 200,000 | +| `SearchTimeout` | 5 s | +| `HeuristicWeight` | 1.0 | +| `ReverseCostMultiplier` | 1.5 | +| `GearSwitchPenaltyMeters` | 1.0 | +| `CurvatureMagnitudeWeight` | 0.10 | +| `CurvatureChangePenaltyMetersPerLevel` | 0.05 | +| `ClearanceCostWeight` | 0.20 | +| `ClearanceCostDistanceMeters` | 0.50 m | + +搜索代价全部以“等效米”为单位: + +```text +primitiveCost = + lengthMeters + × directionMultiplier + × (1 + + CurvatureMagnitudeWeight × abs(curvature / maximumCurvature) + + ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters)) + + gearSwitchPenalty + + CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta) +``` + +其中前进的 `directionMultiplier=1`,倒车使用 `ReverseCostMultiplier`;没有换向时 `gearSwitchPenalty=0`。所有权重必须为有限非负值。默认 `HeuristicWeight=1.0`;若调用方调大该值,只承诺更快地寻找可行解,不承诺离散图上的最低代价。 + +`PlanningResult` 始终返回明确 `PlanningStatus`、诊断信息、零或一条 `IReadOnlyList` 和 `IReadOnlyList`。成功结果中的点契约固定为: + +```text +CoarsePathPoint: + X、Y m + Heading、UnwrappedHeading rad + ArcLength m,非负且不递减 + Direction Forward/Reverse + VehicleCurvature 1/m + BodyClearance m,保守下界 + IsGearSwitchPoint bool + Source Start/MotionPrimitive/GoalTruncation +``` + +`PathSegment` 固定包含 `SegmentIndex`、`Direction`、`StartIndex`、`EndIndex`、`StartsAtGearSwitch` 与 `EndsAtGearSwitch`。`StartIndex` 与 `EndIndex` 都是包含端点的索引,所有分段按索引顺序完整覆盖整条路径。换向时保留两个坐标和航向相同、弧长相同但方向不同的相邻点:前一点结束旧分段,后一点开始新分段并设置 `IsGearSwitchPoint=true`。除这种换向对外,装配器删除相邻重复点。粗路径不包含时间、速度、加速度、舵轮角或轮速。 ### 车辆、碰撞和搜索 - `VehicleKinematics` 根据车辆参数提供最大曲率;直接最大曲率和最小转弯半径同时存在时采用更保守的值。 -- `HeadingFootprintTemplateCache` 为每一个离散航向预计算扩大车辆矩形覆盖的相对栅格偏移;扩大尺寸只在这里应用安全余量。 -- `FootprintCollisionChecker` 依次检查地图边界、距离场快速安全放行和精确矩形模板;它不执行搜索,也不改变地图。 -- `MotionPrimitiveGenerator` 仅生成恒曲率前进/倒车原语,并以不大于 0.05 m 的步长积分,保留内部积分点。 +- `VehicleFootprint` 以连续位姿计算扩大车辆矩形的四角、轴对齐包围盒和外接圆。安全余量只在这里同时加到长度和宽度两侧,不写入地图。 +- `OrientedRectangleCellIntersection` 使用分离轴定理判断连续位姿下的扩大车辆矩形是否与占据栅格矩形相交,不使用仅按离散航向和整数格偏移的模板,因此车辆中心的亚栅格偏移不会漏检。 +- `FootprintCollisionChecker` 依次执行扩大车体边界检查、保守距离场快速放行和包围盒内占据栅格的精确相交检查;它不执行搜索,也不改变地图。 +- `MotionPrimitiveGenerator` 仅生成恒曲率前进/倒车原语,以不大于 0.05 m 的步长保留输出积分点,并使用直线/圆弧解析公式更新位姿,不使用累计误差更大的显式欧拉积分。 +- 相邻碰撞检查位姿的中心位移不得超过 `min(MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2)`。同时用 `0.5 × (中心位移 + 外接圆半径 × 航向变化绝对值)` 作为扫掠附加余量检查相邻区间端点,保守覆盖两个采样位姿之间的车体运动;该附加余量只用于区间碰撞验证,不写入输出车体尺寸。 - `GoalToleranceChecker` 只判断位置、航向和最后一段方向约束;位置和航向阈值来自 `HybridAStarConfiguration`。 -- `GridDijkstraHeuristic` 仅从目标在占据图上生成二维绕障距离启发;它不处理车辆运动学。 -- `HybridAStarSearch` 管理 Open List、Closed Set、节点扩展、代价、父索引与终点选择。Closed Set 键为位置格、航向格、方向和曲率等级。它不拼装最终路径。 -- `PathBacktracker` 从成功节点恢复原语的内部积分点;`CoarsePathAssembler` 去除相邻重复点、累计弧长、标记换向点并构造 `PathSegment`;`CoarsePathValidator` 用同一碰撞规则复核最终稠密输出。 +- 每条原语按积分点顺序执行数值合法性、碰撞和目标检查。如果某个内部积分点满足目标条件,当前原语在该点截断并生成终点候选;候选加入 Open List,只有当它作为最佳有效节点出队时才成功终止搜索。 +- `GridDijkstraHeuristic` 从目标在占据图上生成八邻域二维绕障距离启发,禁止穿过两个对角相邻障碍物的夹角;它不处理车辆运动学。若目标在二维图上不可达,规划返回 `NoFeasiblePath`。 +- `SearchCostCalculator` 只实现本节定义的等效米代价公式,集中处理倒车、换向、曲率、曲率变化与保守净空代价,不管理节点或 Open List。 +- `BinaryMinHeap` 是兼容 `netstandard2.0` 的内部最小堆,不依赖较新运行时的 `PriorityQueue`。排序依次使用 `F`、`H`、较大的 `G` 和单调递增插入序号,保证相同输入的搜索顺序可复现。 +- `HybridAStarSearch` 使用 `Dictionary` 保存每个离散键当前最佳 `G`。发现更小 `G` 时允许重新打开节点;Open List 中的旧条目通过比较最佳 `G` 惰性丢弃。Closed Set 键为位置格、航向格、方向和曲率等级。 +- 搜索循环在扩展节点前检查取消、超时和节点上限。终点候选只有作为当前最佳有效节点出队时才返回成功。 +- `PathBacktracker` 只存父节点索引和原语描述,在成功后确定性地重新生成内部积分点,避免为所有搜索节点长期保存稠密点。`CoarsePathAssembler` 按既定换向规则累计弧长并构造 `PathSegment`。 +- `CoarsePathValidator` 使用相同的连续位姿、扫掠余量和碰撞规则复核最终稠密输出,同时检查有限数值、曲率上限、目标容差、方向约束、弧长单调性、换向对和分段索引完整覆盖。 第一版允许前进、倒车和换向。换向只可发生在原语边界;相邻原语曲率等级最多变化一级。目标达到容差即成功,不尝试 Reeds-Shepp 精确连接。 ## 状态与失败处理 -`PlanningStatus` 至少区分:`Success`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed` 与 `InternalError`。 +`PlanningStatus` 至少区分:`Success`、`Cancelled`、`InvalidRequest`、`InvalidMap`、`MapNotReady`、`InvalidVehicleParameters`、`InvalidCurvatureConfiguration`、`StartOutsideMap`、`StartInCollision`、`GoalOutsideMap`、`GoalInCollision`、`SearchTimeout`、`SearchNodeLimitExceeded`、`NoFeasiblePath`、`BacktrackingFailed`、`FinalValidationFailed` 与 `InternalError`。 -所有输入错误在开始搜索前返回状态与可读原因;搜索或地图对象不能通过异常把部分路径发布给调用方。`PlanningDiagnostics` 记录扩展节点数、生成节点数、总路径长度、最小净空、耗时和终止原因,供之后的性能优化使用。 +所有输入错误在开始搜索前返回状态与可读原因;取消、超时和节点上限均返回空路径,不发布部分结果。除参数为空这类编程错误外,搜索或地图对象不能通过异常把部分路径发布给调用方。`PlanningDiagnostics` 记录扩展节点数、生成节点数、重新打开节点数、丢弃的陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因,供之后的性能优化使用。 ## 测试设计 @@ -217,17 +503,42 @@ public sealed class HybridAStarPlanner | 测试文件/入口 | 覆盖内容 | | --- | --- | | `verify_planning_utils.ps1` | mm/m、deg/rad、角度环绕、车体/世界坐标变换和非法数值。 | -| `verify_planning_map_adapter.ps1` | 圆与轴对齐矩形的中心放置和栅格化、人工与 TwoLeg 图层合并、AMR 自身不占据环境图、边界外保守占据、mm→m 深拷贝、距离场与 `PlanningReady`。 | -| `verify_coarse_path_search.ps1` | 无障碍前进、单障碍绕行、允许倒车的狭窄场景、换向标记、位置/航向容差、越界/起终点碰撞/无解、曲率与 0.05 m 稠密点复核。 | -| `verify_coarse_path_integration.ps1` | 由人工障碍地图构建、适配、规划、最终验证的端到端结果与诊断。 | +| `verify_planning_map_factory.ps1` | 统一来源投影、必需/可选失败策略、来源确定性顺序、空图声明、输入指纹、占据哈希、完整/缓冲区缓存命中、来源版本摘要、静态快照隔离与并发访问。 | +| `verify_planning_map_adapter.ps1` | 圆与轴对齐矩形的包围盒裁剪和格矩形相交、AMR 自身不占据环境图、连续行优先存储、坐标边界、非完整末格、越界保守占据、mm→m 深拷贝、距离场不高估与 `PlanningReady`。 | +| `verify_planning_map_image.ps1` | 从 `PlanningGridMap` 渲染占据格、边界、起终点、车辆和路径叠加;覆盖关闭导出、非法尺寸、像素/文件上限、唯一命名、临时文件清理、PNG 结构与 CRC。 | +| `verify_coarse_path_collision.ps1` | 车体中心位于栅格中心和亚栅格位置时的正交/45°/任意航向,边角擦碰、薄障碍、地图边界、距离场快速放行与原语区间扫掠碰撞。 | +| `verify_coarse_path_search.ps1` | 无障碍前进、0.30 m 非整倍数终点截断、单障碍绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、换向对、±π 航向容差、起点已满足目标、无解、取消、超时、节点上限、节点重新打开与确定性顺序。 | +| `verify_coarse_path_integration.ps1` | `CoarsePathPlanningService` 一次调用完成多来源建图、缓存复用、规划、回溯和最终验证;复核调试开关不改变地图指纹或规划结果。 | +| `benchmark_coarse_path.ps1` | Release 构建下的参考场景耗时、扩展节点数、Open List 峰值与托管内存增量。 | | `MovementTest.MapTest` | 在 Clumsy UI 中显示人工与 TwoLeg 投影后的环境栅格;可选 PNG 导出只用于调试证据。 | -| `MovementTest.CoarsePathTest` | 使用固定可复现实例调用 `HybridAStarPlanner`,绘制地图、起终点、扩大车体检查点和粗路径;不向底盘发送运动命令。 | +| `MovementTest.CoarsePathTest` | 使用固定可复现实例调用 `CoarsePathPlanningService`,绘制地图、起终点、扩大车体检查点和粗路径;`TestStop` 取消规划并清理 Painter,不向底盘发送运动命令。 | 每个新增公共契约均需有成功、边界和失败三类测试。测试场景由 `CoarsePathScenarioFactory` 统一生成,不在 `MovementTest` 中手写地图、原语或搜索细节。 +`MovementTest.CoarsePathTest` 在后台任务中调用同步的 `CoarsePathPlanningService.Plan`,持有专用 `CancellationTokenSource`;`TestStop` 先取消规划,再清理任务引用和 Painter。UI 入口不得在界面线程上执行最长 5 s 的搜索,也不得调用任何底盘运动接口。 + +PowerShell 测试统一使用以下形式执行,绕过本机脚本执行策略差异,并在首个错误处停止: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\
+ + +
+
+

0. 总体概览

+
+
车式运动
车头/车尾方向运动,不考虑蟹行
+
前进 + 倒车
允许在原语边界切换方向
+
空间粗路径
不包含速度、加速度、时间戳
+
+ +

0.1 总体职责边界

+ + + + + + +
模块主要职责不负责
Hybrid A*拓扑可达、无碰撞、基本运动学可行、前进/倒车结构、目标位置与航向接近最终速度、加速度、时间、四轮舵角与轮速
路径后处理回溯、稠密点恢复、关键点保留、弧长重采样、快速曲线平滑、完整复核动态约束和最终轨迹时间参数化
后续SQP终点精确收敛、速度/加速度/角速度/时间戳、换向点停车、动态可跟踪性重新决定绕障侧或凭空增加倒车拓扑
底盘逆解与控制四轮舵角、轮速分配和轨迹跟踪全局绕障搜索
+ +

0.2 总体数据流

+
+
感知障碍物 / 点云 → 过滤、投影到二维
+
二维占据栅格 + 障碍物距离场
+
起点、终点、车辆参数、规划配置
+
二维Dijkstra启发图 + Hybrid A*搜索
+
机会式/强制 Reeds–Shepp 终点连接
+
父节点回溯 + 0.05 m内部积分点恢复
+
关键点保留 + 分方向弧长重采样
+
三次B样条优先,局部Bézier/五次多项式,局部QP兜底
+
最终碰撞、净空、曲率、换向结构校验
+
输出空间粗路径 → 后续SQP
+
+ +
+第一版核心原则:先保证算法完整跑通、路径正确、可解释、可复现;性能优化整体放入第十三步TODO,待前12步稳定后再开展。 +
+
+ +
+

1任务边界与输出定义 已确认

+

1.1 车辆运动模式

+

目标平台为四舵轮AMR,但第一阶段只使用车式运动能力:

+
    +
  • 车辆沿车头或车尾方向运动;
  • +
  • 允许前进、倒车以及换向;
  • +
  • 暂不考虑蟹行、纯横移和任意方向全向运动;
  • +
  • 车体航向角始终表示车头朝向,即使车辆正在倒车。
  • +
+ +

1.2 Hybrid A*最小输出

+

输出空间路径点序列,至少包含:

+
位置:x, y
+车体航向:heading
+运动方向:Forward / Reverse
+累计弧长:s
+参考/几何曲率:kappa
+换向标记:isGearSwitchPoint
+安全净空:clearance
+

不输出速度、加速度、角速度、时间戳、四个舵轮角度与轮速。

+ +

1.3 成功判据

+
    +
  • 路径全程无碰撞且不进入未知区域;
  • +
  • 粗路径基本满足最大曲率约束;
  • +
  • 包含正确绕障侧、前进/后退和换向结构;
  • +
  • 终点位置与航向满足第九步终止规则;
  • +
  • 可作为后续SQP的有效初值。
  • +
+
+ +
+

2地图表达与环境输入 已确认

+

2.1 地图生成流程

+
原始点云 / 障碍信息 + ↓ 过滤噪声、地面、离群点 +投影到二维平面 + ↓ +二维占据栅格 Occupancy Grid + ↓ +障碍物距离场 Distance Field / EDT
+ +

2.2 栅格定义

+
    +
  • 地图分辨率初值:0.05 m
  • +
  • 状态:Free、Occupied、Unknown;
  • +
  • 第一版中Unknown按障碍物处理;
  • +
  • 地图包含Origin、Resolution、Width、Height及Version;
  • +
  • 算法内部不得写死分辨率,统一读取地图参数。
  • +
+ +

2.3 车体碰撞模型

+

AMR使用旋转矩形车体碰撞模型,不能只用中心点或单圆近似。安全余量主要加到车体矩形上:

+
CheckLength = VehicleLength + 2 × SafetyMargin
+CheckWidth  = VehicleWidth  + 2 × SafetyMargin
+

初始安全余量为0.03 m。避免地图膨胀与车体扩大同时重复计算同一余量。

+
+ +
+

3车辆运动模型与最大曲率 已确认

+

3.1 参考点

+

车辆状态参考点采用AMR几何中心。

+ +

3.2 基于路径弧长的运动学

+
dx/ds = cos(theta)
+dy/ds = sin(theta)
+dtheta/ds = kappa
+

其中theta为车头航向角,kappa为车体中心路径曲率。

+ +

3.3 最大曲率来源

+

必须同时支持两种来源:

+
    +
  1. 外部直接输入MaxCurvature,或输入MinTurningRadius并换算kappa_max = 1 / R_min
  2. +
  3. 由四轮转向几何调用ComputeMaxCurvature(VehicleKinematicParameters)计算。
  4. +
+

推荐三种模式:

+
ExternalOnly
+GeometryOnly
+ConservativeMinimum
+

当两种来源同时存在时,建议取更保守的较小值。若均无有效值,则返回配置错误。

+ +
+曲率变化率不在本步骤强制建模。第一版先保证曲率不超过上限;渐变曲率原语和实车舵轮速率约束作为后续升级。 +
+
+ +
+

4Hybrid A*搜索节点状态 已确认

+

4.1 连续状态

+
(x, y, theta, direction, curvatureIndex)
+
    +
  • x, y, theta保持连续值用于运动积分;
  • +
  • direction为Forward或Reverse;
  • +
  • curvatureIndex表示当前离散曲率等级。
  • +
+ +

4.2 Closed Set键

+
(ix, iy, iHeading, direction, curvatureIndex)
+

推荐初始离散:

+
    +
  • 位置Closed Set分辨率:0.05–0.10 m
  • +
  • 航向离散:
  • +
  • 方向和曲率等级均进入键,避免把运动状态不同的节点错误合并。
  • +
+ +

4.3 搜索元数据

+

节点还应保存G/H/F代价、父节点索引、生成当前节点的原语积分点、最小净空、终止原因等。这些属于搜索管理信息,不属于车辆物理状态。

+
+ +
+

5运动原语与扩展规则 已确认

+

5.1 第一版原语集合

+
{ -kappa_max, -0.5 kappa_max, 0, 0.5 kappa_max, kappa_max }
+

每种曲率均支持Forward与Reverse。

+ +

5.2 曲率切换限制

+

相邻原语曲率等级最多变化1级。例如:

+
允许:0 → 0.5κmax
+允许:0.5κmax → κmax
+不允许:κmax → -κmax
+

该限制可减少“左打死后立即右打死”的不合理跳变。

+ +

5.3 换向规则

+
    +
  • 前进/倒车只能在运动原语边界切换;
  • +
  • 起始曲率若上层可提供则使用实际值,否则默认0;
  • +
  • 第一版采用恒定曲率原语。
  • +
+ +
+TODO:当恒定曲率原语造成曲率跳变过大、平滑后仍不可跟踪或实车舵轮变化速度不足时,增加线性渐变曲率原语。 +
+
+ +
+

6原语长度、积分与终止 已确认

+

6.1 固定参数

+
PrimitiveLength = 0.50 m
+IntegrationStep = 0.05 m
+

完整原语最多包含10个内部积分点。只有原语终点进入Open List,内部积分点用于碰撞、净空和最终路径恢复。

+ +

6.2 终止条件

+ + + + + + + +
条件处理
累计长度达到0.50 m生成正常候选节点
任一积分点碰撞原语无效,立即终止
越界或进入未知区域原语无效
中途满足目标规则提前终止并保存实际有效段
Reeds–Shepp连接成功结束搜索
+ +
+TODO:后续根据障碍物距离、目标距离和窄通道自动调整原语长度;碰撞积分步长仍保持0.05 m。 +
+
+ +
+

7碰撞检测与航向栅格模板 已确认

+

7.1 两级碰撞检测

+
    +
  1. 距离场快速放行:若车辆中心到最近障碍物距离大于扩大车体外接圆半径及离散补偿,则该点必然安全;
  2. +
  3. 旋转矩形模板精确检测:距离不足以保证安全时,检查当前航向下扩大车体覆盖的所有栅格。
  4. +
+ +

7.2 外接圆仅用于“安全放行”

+
R_outer = 0.5 × sqrt(CheckLength² + CheckWidth²)
+

D > R_outer表示安全;D ≤ R_outer只表示“无法确定”,不等于碰撞,必须进入矩形模板检查。

+ +

7.3 航向栅格模板的准备

+

航向分辨率5°时,在初始化阶段自动生成72组模板:

+
0°, 5°, 10°, ... , 355°
+

每个模板保存旋转后的扩大车体矩形与地图栅格相交所对应的相对栅格偏移:

+
GridOffset { Dx, Dy }
+

运行时把当前车辆中心转换为栅格坐标,选择最近航向模板,将模板中的相对偏移平移到当前位置并查询占据状态。

+ +

7.4 模板生成规则

+
    +
  • 模板以车体几何中心为原点;
  • +
  • 车体尺寸包含安全余量与少量离散补偿;
  • +
  • 只要栅格方块与旋转矩形有交集,就加入模板;
  • +
  • 不能只判断栅格中心是否在矩形内部,否则可能漏掉边角碰撞;
  • +
  • 真实车体始终是矩形,模板呈阶梯状只是栅格化结果。
  • +
+ +

7.5 每个积分点的处理

+
读取距离场
+  ↓
+可安全放行?——是→继续
+  ↓否
+选择航向模板
+  ↓
+平移模板并查询Occupied / Unknown / 越界
+  ↓
+任一命中→碰撞;全部通过→安全
+
+ +
+

8搜索代价与启发函数 已确认

+

8.1 节点排序

+
f(n) = g(n) + w_h × h(n)
+

第一版HeuristicWeight = 1.3

+ +

8.2 累计代价

+
g_new =
+g_parent
++ C_length
++ C_reverse
++ C_switch
++ C_curvature
++ C_curvatureChange
++ C_obstacle
+
    +
  • 路径长度是基础代价;
  • +
  • 后退系数初值ReversePenalty = 1.3
  • +
  • 换向固定惩罚初值GearSwitchPenalty = 2.0
  • +
  • 曲率代价轻度抑制长期极限转向;
  • +
  • 曲率变化代价偏好平缓动作序列;
  • +
  • 障碍物代价让路径尽量远离障碍物。
  • +
+ +

8.3 双启发

+
h = max(h_2D, h_RS)
+
    +
  • h_2D:从目标反向运行二维Dijkstra得到的绕障距离;
  • +
  • h_RS:基于当前位姿、目标位姿和最小转弯半径的Reeds–Shepp距离。
  • +
+

所有权重必须外部可配置。

+
+ +
+

9终点判定、SQP精修与Reeds–Shepp 已确认

+

9.1 基本终点容差

+
GoalPositionTolerance = 0.15 m
+GoalHeadingTolerance  = 5°
+

在每个0.05 m内部积分点检查位置和航向误差,避免越过目标。

+ +

9.2 容差到达与SQP

+

Hybrid A*可以在容差范围内结束,由后续SQP通过终端约束精确收敛到目标。但必须满足:

+
    +
  • 粗路径已包含正确绕障、倒车和换向拓扑;
  • +
  • 当前终点、目标位姿及末端局部区域无碰撞;
  • +
  • 终点附近存在足够局部调整空间;
  • +
  • SQP只负责局部修正,不负责创造新的换向或改变绕障侧。
  • +
+ +

9.3 目标进入方向

+
GoalDirectionConstraint = Any | Forward | Reverse
+

默认Any。该字段约束最后一段运动方向,不改变目标车头航向定义。

+ +

9.4 Reeds–Shepp模式

+ + + + + +
模式连接失败后的行为适用场景
Disabled不尝试,满足容差即可结束开阔终点、调试对照
Opportunistic失败继续搜索,满足容差仍可交SQP默认、普通导航
RequiredNearGoal失败不能按容差结束,必须继续找可连接节点狭窄钻入、精确泊入
+ +

9.5 自动触发

+
2–5 m:每扩展10个节点尝试一次
+≤2 m:每扩展3个节点尝试一次
+采样碰撞步长:0.05 m
+

连接路径必须检查地图边界、Unknown、扩大车体碰撞、安全净空、最大曲率、方向约束及与当前曲率的衔接。

+ +

9.6 默认策略

+
ReedsSheppMode = Opportunistic
+

无需人工每次开启,由程序自动触发。任务类型可覆盖默认模式。

+
+ +
+

10搜索回溯与原始稠密路径 已确认

+

10.1 回溯内容

+

搜索成功后从终止节点沿父节点索引回溯到起点,反转节点链,并恢复每条原语保存的0.05 m内部积分点。

+ +

10.2 不能只输出0.5 m搜索节点

+

只输出原语终点会丢失圆弧形状、碰撞检查细节和换向局部结构。因此必须复用搜索时已经计算的内部点。

+ +

10.3 路径点字段

+
x, y
+heading, unwrappedHeading
+curvature
+direction
+arcLength
+clearance
+isGearSwitchPoint
+source
+ +

10.4 特殊处理

+
    +
  • 删除相邻原语重复边界点;
  • +
  • 中途终止原语只保留真实有效积分点;
  • +
  • Reeds–Shepp连接点追加到普通搜索路径之后;
  • +
  • 机会式容差终点不允许在本步骤被“硬改”为目标点;
  • +
  • 航向同时保存归一化值和连续解缠值;
  • +
  • 累计弧长始终单调增加,方向单独保存;
  • +
  • 按换向点划分Forward/Reverse连续段。
  • +
+
+ +
+

11关键点保留与弧长重采样 已确认

+

11.1 处理目标

+

原始0.05 m稠密路径不直接全部送入平滑和SQP。先保留关键结构,再按照累计弧长重采样。

+ +

11.2 必须保留的关键点

+
    +
  • 起点与终点;
  • +
  • 所有换向点;
  • +
  • 曲率等级变化点;
  • +
  • 直线/弯道切换点;
  • +
  • Reeds–Shepp分段和连接点;
  • +
  • 障碍物附近最小净空点;
  • +
  • 必要的明显航向变化点。
  • +
+ +

11.3 分方向重采样

+

每个Forward或Reverse段独立处理,不能跨越换向点插值。

+ +

11.4 推荐间距

+
普通区域:0.10 m
+重点区域:0.05 m
+

以下区域采用0.05 m:

+
    +
  • 障碍距离小于0.50 m;
  • +
  • |κ| ≥ 0.5 κmax;
  • +
  • 目标2 m范围内;
  • +
  • 换向点前后0.30 m;
  • +
  • 曲率变化点前后0.25 m;
  • +
  • Reeds–Shepp连接段与窄通道。
  • +
+ +

11.5 重采样后复核

+

新插值点必须重新执行车体碰撞检查。最终输出点可为0.10 m,但碰撞验证内部步长仍不得大于0.05 m。

+
+ +
+

12快速曲线平滑、局部QP兜底与最终校验 已确认

+

12.1 最终采用的分层方案

+
+
按Forward/Reverse方向段拆分
+
普通路径段:三次B样条快速平滑
+
局部短转角:分段三次Bézier
+
末端短连接:五次多项式(可选)
+
0.05 m完整碰撞、净空、曲率验证
+
失败局部:安全走廊约束QP
+
局部QP仍失败:回退第十一步原始可行路径
+
+ +

12.2 为什么不默认全路径QP

+

全路径多轮QP还需要走廊生成、曲率线性化和反复碰撞检测,可能与后续SQP功能重复并增加耗时。因此第一版采用快速曲线优先,仅对失败局部使用QP。

+ +

12.3 平滑结构约束

+
    +
  • 换向点固定,不能使用一条曲线跨越换向;
  • +
  • 不改变绕障侧、前进/后退顺序和换向次数;
  • +
  • Reeds–Shepp精确目标点固定;
  • +
  • 容差终点不在本步骤强行修改为目标;
  • +
  • 障碍物附近控制点移动范围收紧。
  • +
+ +

12.4 典型偏移范围

+
普通区域最大偏移:0.10–0.15 m
+障碍物附近最大偏移:0.02–0.05 m
+局部QP问题区间:失败点前后各约1.0 m
+ +

12.5 航向与曲率

+
    +
  • 平滑后必须根据新几何路径重新计算切线与航向;
  • +
  • 前进段车头航向与路径切线一致;
  • +
  • 倒车段车头航向与路径切线相差π;
  • +
  • 区分几何曲率与车辆控制曲率;
  • +
  • 建议平滑后最大车辆曲率不超过0.95 κmax
  • +
+ +

12.6 最终验证

+
    +
  • 0.05 m采样完整车体碰撞检测;
  • +
  • Unknown与越界检查;
  • +
  • 安全余量检查;
  • +
  • 最大曲率与曲率变化检查;
  • +
  • 起终点、换向点、方向分段和路径连续性检查。
  • +
+
+ +
+

13性能优化阶段 整体TODO

+

第十三步不作为第一版主流程实现要求。前12步完整跑通并建立正确性基线后,再进行性能剖析与针对性优化。

+ +

13.1 第一版仅保留防失控措施

+
    +
  • 较宽松的搜索超时;
  • +
  • 最大扩展节点和最大生成节点限制;
  • +
  • 取消请求机制;
  • +
  • 各阶段基础耗时与节点数量统计。
  • +
+ +

13.2 后续性能TODO

+
    +
  1. 统计地图处理、Dijkstra、Hybrid A*、碰撞检测、Reeds–Shepp、回溯和平滑耗时;
  2. +
  3. 根据剖析结果识别真实瓶颈;
  4. +
  5. 优化Open List、Closed Set、节点内存与对象分配;
  6. +
  7. 预计算运动原语、航向模板、距离场和启发图;
  8. +
  9. 地图裁剪、路径热启动、增量更新、多分辨率搜索;
  10. +
  11. 最终验证平均、P95、最坏时间和100 ms达成率。
  12. +
+ +
+开发顺序:正确性 → 可行性 → 可复现性 → 性能剖析 → 针对性优化 → 100 ms验收。 +
+
+ +
+

14输入输出接口、错误状态与验收 已确认

+

14.1 推荐模块

+
HybridAStarPlanner
+├── MapProcessor
+├── VehicleModel
+├── MotionPrimitiveGenerator
+├── CollisionChecker
+├── HeuristicProvider
+├── ReedsSheppConnector
+├── GoalChecker
+├── PathBacktracker
+├── PathResampler
+├── PathSmoother
+├── PathValidator
+└── PlanningDiagnostics
+ +

14.2 主接口

+
public interface IHybridAStarPlanner
+{
+    PlanningResult Plan(PlanningRequest request);
+}
+ +

14.3 输入

+
    +
  • 二维占据栅格;
  • +
  • 起点位姿与可选起始方向、起始曲率;
  • +
  • 目标位姿与目标进入方向;
  • +
  • 车辆尺寸、安全余量、最大曲率/最小转弯半径;
  • +
  • Hybrid A*配置、Reeds–Shepp模式和平滑配置。
  • +
+ +

14.4 输出

+

输出不带时间的空间路径点和分段信息:

+
X, Y
+Heading, UnwrappedHeading
+ArcLength
+GeometricCurvature
+VehicleCurvature
+Direction
+BodyClearance
+IsGearSwitchPoint
+Source
+ +

14.5 终点到达类型

+
ExactByReedsShepp
+ReachedWithinTolerance
+ExactAfterDirectSearch
+ +

14.6 主要返回状态

+
Success
+SuccessWithToleranceGoal
+SuccessWithSmoothingFallback
+InvalidRequest / InvalidMap / InvalidVehicleParameters
+StartOutsideMap / StartInCollision / StartInUnknownArea
+GoalOutsideMap / GoalInCollision / GoalInUnknownArea
+InvalidCurvatureConfiguration
+SearchTimeout / SearchNodeLimitExceeded / NoFeasiblePath
+ReedsSheppRequiredButFailed
+BacktrackingFailed / ResamplingFailed / FinalValidationFailed
+Cancelled / InternalError
+ +

14.7 第一版验收重点

+
    +
  • 有效场景能输出完整路径;
  • +
  • 起点一致,终点满足第九步规则;
  • +
  • 完整车体全程无碰撞且不进入Unknown;
  • +
  • 最大曲率不超过车辆上限;
  • +
  • 前进、倒车和换向结构正确;
  • +
  • 回溯无断点和异常重复;
  • +
  • 平滑失败可以回退原路径;
  • +
  • 失败场景返回明确状态;
  • +
  • 后续SQP能够读取并收敛。
  • +
+
+ +
+

附录A:第一版推荐配置参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
参数推荐初值说明
MapResolution0.05 m读取地图配置,不写死
HeadingResolution72个航向模板
SafetyMargin0.03 m主要加在车体矩形
PrimitiveLength0.50 m第一版固定
IntegrationStep0.05 m碰撞与积分步长
CurvatureLevels-1,-0.5,0,0.5,1 × κmax5级
GoalPositionTolerance0.15 mHybrid A*基础容差
GoalHeadingToleranceHybrid A*基础容差
HeuristicWeight1.3Weighted A*
ReversePenalty1.3允许倒车但略微惩罚
GearSwitchPenalty2.0减少频繁换向
ReedsSheppModeOpportunistic默认自动机会式
AnalyticExpansionDistance5.0 m进入后周期尝试
NearGoalDistance2.0 m提高尝试频率
AnalyticExpansionInterval10节点2–5 m
NearGoalInterval3节点≤2 m
NormalResampleSpacing0.10 m普通区域
FineResampleSpacing0.05 m重点区域
FineObstacleDistance0.50 m小于此值加密
FineGoalDistance2.0 m目标附近加密
GearSwitchDenseRange±0.30 m换向点附近
CurvatureTransitionDenseRange±0.25 m曲率切换附近
CurvatureLimitRatio0.95平滑后留控制余量
SearchTimeLimitMs3000–5000 ms第一版防失控,不是性能指标
+
+ +
+

附录B:总体伪代码

+
PlanningResult Plan(request)
+{
+    ValidateRequest(request);
+    ValidateMap(request.Map);
+    ResolveVehicleCurvatureLimit(request.Vehicle);
+    ValidateStartAndGoal();
+
+    PrepareDistanceFieldIfNeeded();
+    PrepareHeadingFootprintTemplatesIfNeeded();
+    BuildGoalDijkstraHeuristic();
+
+    startNode = CreateStartNode();
+    PushOpen(startNode);
+
+    while (OpenList not empty)
+    {
+        CheckCancellationAndSafetyLimits();
+
+        current = PopBestValidNode();
+
+        if (ShouldTryReedsShepp(current))
+        {
+            rsPath = TryReedsShepp(current, goal);
+            if (ValidateAnalyticPath(rsPath))
+                return BuildFinalResult(current, rsPath);
+        }
+
+        if (GoalChecker.CanTerminateByTolerance(current))
+            return BuildFinalResult(current, noAnalyticPath);
+
+        foreach (primitive in GenerateAllowedPrimitives(current))
+        {
+            integrated = IntegratePrimitive(
+                current,
+                primitive,
+                step = 0.05 m,
+                maxLength = 0.50 m);
+
+            if (!integrated.Valid)
+                continue;
+
+            child = CreateChildNode(integrated);
+            if (!ImproveBestCost(child))
+                continue;
+
+            PushOpen(child);
+        }
+    }
+
+    return Failure(NoFeasiblePath);
+}
+
+BuildFinalResult(goalNode, analyticPath)
+{
+    rawDense = BacktrackAndRestorePrimitivePoints(goalNode);
+    AppendAnalyticPathIfAny(rawDense, analyticPath);
+
+    segmented = SplitByMotionDirection(rawDense);
+    resampled = PreserveKeyPointsAndResample(segmented);
+
+    smoothed = TryFastSplineSmoothing(resampled);
+    if (!ValidatePath(smoothed))
+        smoothed = TryLocalCurveOrQPFallback(resampled);
+
+    if (!ValidatePath(smoothed))
+        smoothed = resampled;
+
+    if (!ValidatePath(smoothed))
+        return Failure(FinalValidationFailed);
+
+    return Success(smoothed, diagnostics);
+}
+
+ +
+

附录C:建议开发里程碑

+ + + + + + + + + + + +
阶段范围完成标准
M0 数据与工具Pose、地图、车辆参数、角度/坐标工具单元测试通过
M1 运动学与碰撞恒曲率积分、航向模板、矩形碰撞可视化验证不同角度无漏检
M2 最小Hybrid A*无障碍、只前进、基础启发稳定从起点到终点
M3 障碍与倒车Dijkstra、前进/倒车、换向代价绕障与一次倒车场景通过
M4 终点模块容差、GoalDirection、Reeds–Shepp三模式普通与狭窄目标场景通过
M5 后处理回溯、0.05 m稠密点、分段、重采样路径结构无断点、换向明确
M6 平滑与复核B样条、Bézier、局部QP兜底不改变拓扑,失败可回退
M7 接口与集成PlanningRequest/Result、状态、诊断可接入SQP
M8 性能TODO剖析、优化、100 ms目标在正确性基线后执行
+
+
+