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; IReadOnlyList targets = BuildTargets(options, segmentLength); double[] ratios = { 0.5d, 0.4d, 0.6d }; for (int ratioIndex = 0; ratioIndex < ratios.Length; ratioIndex++) { for (int targetIndex = 0; targetIndex < targets.Count; targetIndex++) { if (variants.Count >= options.MaximumCandidatesPerRegion) return new ReadOnlyCollection(variants); AddIfLegal( variants, targets[targetIndex], ratios[ratioIndex], anchor, firstEvent, lastEvent, segmentLength, ratioIndex == 0, 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, options.MinimumWindowLengthMeters, options.MaximumWindowLengthMeters, 0.75d * options.PreferredWindowLengthMeters, 1.25d * options.PreferredWindowLengthMeters, }; 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; case "InteriorCoverage": transitions = new[] { Transition(1d, 0) }; segmentLength = 2d; 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; } IReadOnlyList firstVariants = regions[0].WindowVariants; bool representativeTargetsFirst = firstVariants.Count >= 3 && Math.Abs(WindowLength(firstVariants[0]) - 0.50d) <= 1e-9d && Math.Abs(WindowLength(firstVariants[1]) - 0.20d) <= 1e-9d && Math.Abs(WindowLength(firstVariants[2]) - 0.80d) <= 1e-9d; bool hasAsymmetricVariant = false; for (int index = 0; index < firstVariants.Count; index++) { if (Math.Abs( firstVariants[index].LeftWindowLengthMeters - firstVariants[index].RightWindowLengthMeters) > 1e-9d) { hasAsymmetricVariant = true; break; } } LocalG2WindowVariant first = firstVariants[0]; return new WindowPlanningTestSnapshot( regions.Count, string.Join(",", counts), maximumLength, exactEnvelope, first.LeftWindowLengthMeters, first.RightWindowLengthMeters, string.Join("|", signature), firstVariants.Count, representativeTargetsFirst, hasAsymmetricVariant); } public sealed class WindowPlanningTestSnapshot { internal WindowPlanningTestSnapshot( int regionCount, string transitionCounts, double maximumWindowLength, bool exactEnvelope, double firstLeftLength, double firstRightLength, string signature, int firstRegionVariantCount, bool representativeTargetsFirst, bool hasAsymmetricVariant) { RegionCount = regionCount; TransitionCounts = transitionCounts; MaximumWindowLength = maximumWindowLength; ExactEnvelope = exactEnvelope; FirstLeftLength = firstLeftLength; FirstRightLength = firstRightLength; Signature = signature; FirstRegionVariantCount = firstRegionVariantCount; RepresentativeTargetsFirst = representativeTargetsFirst; HasAsymmetricVariant = hasAsymmetricVariant; } 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; } public int FirstRegionVariantCount { get; } public bool RepresentativeTargetsFirst { get; } public bool HasAsymmetricVariant { get; } } private static double WindowLength(LocalG2WindowVariant variant) => variant.EndArcLengthMeters - variant.StartArcLengthMeters; 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), }); } } }