refactor: simplify GeometrySimplifier by removing wrappers and extracting shared helpers
Remove pass-through wrappers (FitWithStartTangent, MaxRadialDeviation), extract PerpendicularDistance and NormalizeAngle helpers to deduplicate mirror axis math, convert GetExitDirection to switch expression, and simplify ComputeEndTangent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,13 +134,11 @@ public class GeometrySimplifier
|
|||||||
|
|
||||||
if (midpoints.Count < 4) return MirrorAxisResult.None;
|
if (midpoints.Count < 4) return MirrorAxisResult.None;
|
||||||
|
|
||||||
// Centroid
|
var centroid = new Vector(
|
||||||
var cx = 0.0;
|
midpoints.Average(p => p.X),
|
||||||
var cy = 0.0;
|
midpoints.Average(p => p.Y));
|
||||||
foreach (var p in midpoints) { cx += p.X; cy += p.Y; }
|
var cx = centroid.X;
|
||||||
cx /= midpoints.Count;
|
var cy = centroid.Y;
|
||||||
cy /= midpoints.Count;
|
|
||||||
var centroid = new Vector(cx, cy);
|
|
||||||
|
|
||||||
// Covariance matrix for PCA
|
// Covariance matrix for PCA
|
||||||
var cxx = 0.0;
|
var cxx = 0.0;
|
||||||
@@ -192,12 +190,25 @@ public class GeometrySimplifier
|
|||||||
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
|
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double NormalizeAngle(double angle) =>
|
||||||
|
angle < 0 ? angle + Angle.TwoPI : angle;
|
||||||
|
|
||||||
private static Vector Normalize(Vector v)
|
private static Vector Normalize(Vector v)
|
||||||
{
|
{
|
||||||
var len = System.Math.Sqrt(v.X * v.X + v.Y * v.Y);
|
var len = System.Math.Sqrt(v.X * v.X + v.Y * v.Y);
|
||||||
return len < 1e-10 ? new Vector(1, 0) : new Vector(v.X / len, v.Y / len);
|
return len < 1e-10 ? new Vector(1, 0) : new Vector(v.X / len, v.Y / len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double PerpendicularDistance(Vector point, Vector axisPoint, Vector axisDir)
|
||||||
|
{
|
||||||
|
var dx = point.X - axisPoint.X;
|
||||||
|
var dy = point.Y - axisPoint.Y;
|
||||||
|
var dot = dx * axisDir.X + dy * axisDir.Y;
|
||||||
|
var px = dx - dot * axisDir.X;
|
||||||
|
var py = dy - dot * axisDir.Y;
|
||||||
|
return System.Math.Sqrt(px * px + py * py);
|
||||||
|
}
|
||||||
|
|
||||||
private static double MirrorMatchScore(List<Vector> points, Vector axisPoint, Vector axisDir)
|
private static double MirrorMatchScore(List<Vector> points, Vector axisPoint, Vector axisDir)
|
||||||
{
|
{
|
||||||
var matchTol = 0.1;
|
var matchTol = 0.1;
|
||||||
@@ -206,14 +217,7 @@ public class GeometrySimplifier
|
|||||||
for (var i = 0; i < points.Count; i++)
|
for (var i = 0; i < points.Count; i++)
|
||||||
{
|
{
|
||||||
var p = points[i];
|
var p = points[i];
|
||||||
|
var dist = PerpendicularDistance(p, axisPoint, axisDir);
|
||||||
// Distance from point to axis
|
|
||||||
var dx = p.X - axisPoint.X;
|
|
||||||
var dy = p.Y - axisPoint.Y;
|
|
||||||
var dot = dx * axisDir.X + dy * axisDir.Y;
|
|
||||||
var perpX = dx - dot * axisDir.X;
|
|
||||||
var perpY = dy - dot * axisDir.Y;
|
|
||||||
var dist = System.Math.Sqrt(perpX * perpX + perpY * perpY);
|
|
||||||
|
|
||||||
// Points on the axis count as matched
|
// Points on the axis count as matched
|
||||||
if (dist < matchTol)
|
if (dist < matchTol)
|
||||||
@@ -223,14 +227,12 @@ public class GeometrySimplifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reflect across axis and look for partner
|
// Reflect across axis and look for partner
|
||||||
var mx = p.X - 2 * perpX;
|
var reflected = new MirrorAxisResult(axisPoint, axisDir, 0).Reflect(p);
|
||||||
var my = p.Y - 2 * perpY;
|
|
||||||
|
|
||||||
for (var j = 0; j < points.Count; j++)
|
for (var j = 0; j < points.Count; j++)
|
||||||
{
|
{
|
||||||
if (i == j) continue;
|
if (i == j) continue;
|
||||||
var d = System.Math.Sqrt((points[j].X - mx) * (points[j].X - mx) +
|
var d = reflected.DistanceTo(points[j]);
|
||||||
(points[j].Y - my) * (points[j].Y - my));
|
|
||||||
if (d < matchTol)
|
if (d < matchTol)
|
||||||
{
|
{
|
||||||
matched++;
|
matched++;
|
||||||
@@ -259,14 +261,7 @@ public class GeometrySimplifier
|
|||||||
|
|
||||||
var ci = candidates[i];
|
var ci = candidates[i];
|
||||||
var ciCenter = ci.BoundingBox.Center;
|
var ciCenter = ci.BoundingBox.Center;
|
||||||
|
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis
|
||||||
// Distance from candidate center to axis
|
|
||||||
var dx = ciCenter.X - axis.Point.X;
|
|
||||||
var dy = ciCenter.Y - axis.Point.Y;
|
|
||||||
var dot = dx * axis.Direction.X + dy * axis.Direction.Y;
|
|
||||||
var perpDist = System.Math.Sqrt((dx - dot * axis.Direction.X) * (dx - dot * axis.Direction.X) +
|
|
||||||
(dy - dot * axis.Direction.Y) * (dy - dot * axis.Direction.Y));
|
|
||||||
if (perpDist < 0.1) continue; // on the axis
|
|
||||||
|
|
||||||
var mirrorCenter = axis.Reflect(ciCenter);
|
var mirrorCenter = axis.Reflect(ciCenter);
|
||||||
|
|
||||||
@@ -328,12 +323,8 @@ public class GeometrySimplifier
|
|||||||
var mirrorEp = axis.Reflect(ep);
|
var mirrorEp = axis.Reflect(ep);
|
||||||
|
|
||||||
// Mirroring reverses winding — swap start/end to preserve arc direction
|
// Mirroring reverses winding — swap start/end to preserve arc direction
|
||||||
var mirrorStart = System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X);
|
var mirrorStart = NormalizeAngle(System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X));
|
||||||
var mirrorEnd = System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X);
|
var mirrorEnd = NormalizeAngle(System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X));
|
||||||
|
|
||||||
// Normalize to [0, 2pi)
|
|
||||||
if (mirrorStart < 0) mirrorStart += Angle.TwoPI;
|
|
||||||
if (mirrorEnd < 0) mirrorEnd += Angle.TwoPI;
|
|
||||||
|
|
||||||
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
|
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
|
||||||
result.Layer = arc.Layer;
|
result.Layer = arc.Layer;
|
||||||
@@ -357,15 +348,16 @@ public class GeometrySimplifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
chainedTangent = ComputeEndTangent(result.Center, result.Points);
|
chainedTangent = ComputeEndTangent(result.Center, result.Points);
|
||||||
|
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
|
||||||
candidates.Add(new ArcCandidate
|
candidates.Add(new ArcCandidate
|
||||||
{
|
{
|
||||||
StartIndex = j,
|
StartIndex = j,
|
||||||
EndIndex = result.EndIndex,
|
EndIndex = result.EndIndex,
|
||||||
FittedArc = CreateArc(result.Center, result.Radius, result.Points, entities[j]),
|
FittedArc = arc,
|
||||||
MaxDeviation = result.Deviation,
|
MaxDeviation = result.Deviation,
|
||||||
BoundingBox = result.Points.GetBoundingBox(),
|
BoundingBox = result.Points.GetBoundingBox(),
|
||||||
FirstPoint = result.Points[0],
|
FirstPoint = arc.StartPoint(),
|
||||||
LastPoint = result.Points[^1],
|
LastPoint = arc.EndPoint(),
|
||||||
});
|
});
|
||||||
|
|
||||||
j = result.EndIndex + 1;
|
j = result.EndIndex + 1;
|
||||||
@@ -386,14 +378,16 @@ public class GeometrySimplifier
|
|||||||
? chainedTangent
|
? chainedTangent
|
||||||
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
||||||
|
|
||||||
var (center, radius, dev) = TryFit(points, startTangent);
|
var endTangent = GetExitDirection(entities[k]);
|
||||||
|
var (center, radius, dev) = TryFit(points, startTangent, endTangent);
|
||||||
if (!center.IsValid()) return null;
|
if (!center.IsValid()) return null;
|
||||||
|
|
||||||
// Extend the arc as far as possible
|
// Extend the arc as far as possible
|
||||||
while (k + 1 <= runEnd)
|
while (k + 1 <= runEnd)
|
||||||
{
|
{
|
||||||
var extPoints = CollectPoints(entities, start, k + 1);
|
var extPoints = CollectPoints(entities, start, k + 1);
|
||||||
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent) : (Vector.Invalid, 0, 0d);
|
var extEndTangent = GetExitDirection(entities[k + 1]);
|
||||||
|
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d);
|
||||||
if (!nc.IsValid()) break;
|
if (!nc.IsValid()) break;
|
||||||
|
|
||||||
k++;
|
k++;
|
||||||
@@ -413,9 +407,23 @@ public class GeometrySimplifier
|
|||||||
return new ArcFitResult(center, radius, dev, points, k);
|
return new ArcFitResult(center, radius, dev, points, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent)
|
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent)
|
||||||
{
|
{
|
||||||
var (center, radius, dev) = FitWithStartTangent(points, startTangent);
|
// Try dual-tangent fit first (matches direction at both endpoints)
|
||||||
|
if (endTangent.IsValid())
|
||||||
|
{
|
||||||
|
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent);
|
||||||
|
if (dc.IsValid() && dd <= Tolerance)
|
||||||
|
{
|
||||||
|
var isRev = SumSignedAngles(dc, points) < 0;
|
||||||
|
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
|
||||||
|
if (aDev <= Tolerance)
|
||||||
|
return (dc, dr, System.Math.Max(dd, aDev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to start-tangent-only, then mirror axis
|
||||||
|
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
(center, radius, dev) = FitMirrorAxis(points);
|
(center, radius, dev) = FitMirrorAxis(points);
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
@@ -430,16 +438,6 @@ public class GeometrySimplifier
|
|||||||
return (center, radius, System.Math.Max(dev, arcDev));
|
return (center, radius, System.Math.Max(dev, arcDev));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fits a circular arc constrained to be tangent to the given direction at the
|
|
||||||
/// first point. The center lies at the intersection of the normal at P1 (perpendicular
|
|
||||||
/// to the tangent) and the perpendicular bisector of the chord P1->Pn, guaranteeing
|
|
||||||
/// the arc passes through both endpoints and departs P1 in the given direction.
|
|
||||||
/// </summary>
|
|
||||||
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
|
||||||
List<Vector> points, Vector tangent) =>
|
|
||||||
ArcFit.FitWithStartTangent(points, tangent);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the tangent direction at the last point of a fitted arc,
|
/// Computes the tangent direction at the last point of a fitted arc,
|
||||||
/// used to chain tangent continuity to the next arc.
|
/// used to chain tangent continuity to the next arc.
|
||||||
@@ -447,15 +445,10 @@ public class GeometrySimplifier
|
|||||||
private static Vector ComputeEndTangent(Vector center, List<Vector> points)
|
private static Vector ComputeEndTangent(Vector center, List<Vector> points)
|
||||||
{
|
{
|
||||||
var lastPt = points[^1];
|
var lastPt = points[^1];
|
||||||
var totalAngle = SumSignedAngles(center, points);
|
|
||||||
|
|
||||||
var rx = lastPt.X - center.X;
|
var rx = lastPt.X - center.X;
|
||||||
var ry = lastPt.Y - center.Y;
|
var ry = lastPt.Y - center.Y;
|
||||||
|
var sign = SumSignedAngles(center, points) >= 0 ? 1 : -1;
|
||||||
if (totalAngle >= 0)
|
return new Vector(-sign * ry, sign * rx);
|
||||||
return new Vector(-ry, rx);
|
|
||||||
else
|
|
||||||
return new Vector(ry, -rx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -496,12 +489,12 @@ public class GeometrySimplifier
|
|||||||
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
||||||
|
|
||||||
var dOpt = GoldenSectionMin(dInit - range, dInit + range,
|
var dOpt = GoldenSectionMin(dInit - range, dInit + range,
|
||||||
d => MaxRadialDeviation(points, mx + d * nx, my + d * ny,
|
d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny,
|
||||||
System.Math.Sqrt(halfChord * halfChord + d * d)));
|
System.Math.Sqrt(halfChord * halfChord + d * d)));
|
||||||
|
|
||||||
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
||||||
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
||||||
return (center, radius, MaxRadialDeviation(points, center.X, center.Y, radius));
|
return (center, radius, ArcFit.MaxRadialDeviation(points, center.X, center.Y, radius));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double GoldenSectionMin(double low, double high, Func<double, double> eval)
|
private static double GoldenSectionMin(double low, double high, Func<double, double> eval)
|
||||||
@@ -554,20 +547,28 @@ public class GeometrySimplifier
|
|||||||
var firstPoint = points[0];
|
var firstPoint = points[0];
|
||||||
var lastPoint = points[^1];
|
var lastPoint = points[^1];
|
||||||
|
|
||||||
var startAngle = System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X);
|
var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X));
|
||||||
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
|
var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X));
|
||||||
var isReversed = SumSignedAngles(center, points) < 0;
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
|
||||||
// Normalize to [0, 2pi)
|
|
||||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
|
||||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
|
||||||
|
|
||||||
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||||
arc.Layer = sourceEntity.Layer;
|
arc.Layer = sourceEntity.Layer;
|
||||||
arc.Color = sourceEntity.Color;
|
arc.Color = sourceEntity.Color;
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the exit direction (tangent at endpoint) of an entity.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector GetExitDirection(Entity entity) => entity switch
|
||||||
|
{
|
||||||
|
Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y),
|
||||||
|
Arc arc => arc.IsReversed
|
||||||
|
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
|
||||||
|
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sums signed angular change traversing consecutive points around a center.
|
/// Sums signed angular change traversing consecutive points around a center.
|
||||||
/// Positive = CCW, negative = CW.
|
/// Positive = CCW, negative = CW.
|
||||||
@@ -587,12 +588,6 @@ public class GeometrySimplifier
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Max deviation of intermediate points (excluding endpoints) from a circle.
|
|
||||||
/// </summary>
|
|
||||||
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
|
||||||
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures the maximum distance from sampled points along the fitted arc
|
/// Measures the maximum distance from sampled points along the fitted arc
|
||||||
/// back to the original line segments. This catches cases where points lie
|
/// back to the original line segments. This catches cases where points lie
|
||||||
|
|||||||
Reference in New Issue
Block a user