fix: handle degenerate local G2 derivative hulls

This commit is contained in:
梁薄云
2026-07-30 17:53:37 +08:00
parent 9edb80e552
commit 879e30a025
2 changed files with 66 additions and 2 deletions
@@ -389,16 +389,51 @@ internal sealed class LocalG2CandidateBuilder
private static bool ContainsOrigin(DerivativeControlPoint first, DerivativeControlPoint second, private static bool ContainsOrigin(DerivativeControlPoint first, DerivativeControlPoint second,
DerivativeControlPoint third, double margin) 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 crossFirstSecond = Cross(first, second);
double crossSecondThird = Cross(second, third); double crossSecondThird = Cross(second, third);
double crossThirdFirst = Cross(third, first); double crossThirdFirst = Cross(third, first);
if (!NumericGuard.IsFinite(crossFirstSecond) || !NumericGuard.IsFinite(crossSecondThird) || !NumericGuard.IsFinite(crossThirdFirst)) if (!NumericGuard.IsFinite(triangleArea) || !NumericGuard.IsFinite(crossFirstSecond) ||
!NumericGuard.IsFinite(crossSecondThird) || !NumericGuard.IsFinite(crossThirdFirst))
{
return true; return true;
}
double areaMargin = margin * Math.Max(1d, Math.Max(first.Norm, Math.Max(second.Norm, third.Norm))); 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) || return (crossFirstSecond >= -areaMargin && crossSecondThird >= -areaMargin && crossThirdFirst >= -areaMargin) ||
(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) private static double DistanceToSegment(DerivativeControlPoint start, DerivativeControlPoint end)
{ {
double dx = end.X - start.X; double dx = end.X - start.X;
@@ -523,6 +558,7 @@ internal sealed class LocalG2CandidateBuilder
case "Spliced": return BuildSpliced(); case "Spliced": return BuildSpliced();
case "StartBoundary": return BuildStartBoundary(); case "StartBoundary": return BuildStartBoundary();
case "InteriorStationaryCurve": return BuildInteriorStationaryCurve(); case "InteriorStationaryCurve": return BuildInteriorStationaryCurve();
case "ConstantVelocityCurve": return BuildConstantVelocityCurve();
case "ExactSpliceEndpoints": return BuildExactSpliceEndpoints(); case "ExactSpliceEndpoints": return BuildExactSpliceEndpoints();
case "GearBoundary": return BuildGearBoundary(); case "GearBoundary": return BuildGearBoundary();
default: throw new ArgumentOutOfRangeException(nameof(scenario)); default: throw new ArgumentOutOfRangeException(nameof(scenario));
@@ -536,7 +572,7 @@ internal sealed class LocalG2CandidateBuilder
int outputRegionCount, bool internalConnectionsAreG2, string direction, int outputRegionCount, bool internalConnectionsAreG2, string direction,
bool vehicleAndGeometricCurvatureSignsAreOpposite, bool noDuplicateNonGearPoints, bool vehicleAndGeometricCurvatureSignsAreOpposite, bool noDuplicateNonGearPoints,
bool endpointsUnchanged, bool rejected = false, bool endpointsAreExact = false, bool endpointsUnchanged, bool rejected = false, bool endpointsAreExact = false,
bool gearBoundaryMarkerPreserved = false) bool gearBoundaryMarkerPreserved = false, bool accepted = false)
{ {
CandidateCount = candidateCount; CandidateCount = candidateCount;
StartPositionError = startPositionError; StartPositionError = startPositionError;
@@ -553,6 +589,7 @@ internal sealed class LocalG2CandidateBuilder
Rejected = rejected; Rejected = rejected;
EndpointsAreExact = endpointsAreExact; EndpointsAreExact = endpointsAreExact;
GearBoundaryMarkerPreserved = gearBoundaryMarkerPreserved; GearBoundaryMarkerPreserved = gearBoundaryMarkerPreserved;
Accepted = accepted;
} }
public int CandidateCount { get; } public int CandidateCount { get; }
public double StartPositionError { get; } public double StartPositionError { get; }
@@ -569,6 +606,7 @@ internal sealed class LocalG2CandidateBuilder
public bool Rejected { get; } public bool Rejected { get; }
public bool EndpointsAreExact { get; } public bool EndpointsAreExact { get; }
public bool GearBoundaryMarkerPreserved { get; } public bool GearBoundaryMarkerPreserved { get; }
public bool Accepted { get; }
} }
private static CandidateTestSnapshot BuildIsolated() private static CandidateTestSnapshot BuildIsolated()
@@ -711,6 +749,28 @@ internal sealed class LocalG2CandidateBuilder
false, false, false, !accepted); 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<SmoothingPoint2D>();
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() private static CandidateTestSnapshot BuildExactSpliceEndpoints()
{ {
PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null); PreparedDirectionSegment segment = CreateSegment(TravelDirection.Forward, null);
@@ -62,6 +62,10 @@ $interiorDerivative = Invoke-Scenario 'InteriorStationaryCurve'
Assert-True $interiorDerivative.Rejected ` Assert-True $interiorDerivative.Rejected `
'A curve with a stationary interior derivative must be rejected even when its endpoint chord is short.' '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' $exactSplice = Invoke-Scenario 'ExactSpliceEndpoints'
Assert-True $exactSplice.EndpointsAreExact ` Assert-True $exactSplice.EndpointsAreExact `
'Splicing must write interpolated exact endpoints instead of accepting approximate candidate endpoints.' 'Splicing must write interpolated exact endpoints instead of accepting approximate candidate endpoints.'