feat: add GeometrySimplifier.Analyze with incremental arc fitting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 23:30:10 -04:00
parent bbc02f6f3f
commit ca5eb53bc1
2 changed files with 225 additions and 1 deletions

View File

@@ -23,7 +23,30 @@ public class GeometrySimplifier
public List<ArcCandidate> Analyze(Shape shape)
{
throw new NotImplementedException();
var candidates = new List<ArcCandidate>();
var entities = shape.Entities;
var i = 0;
while (i < entities.Count)
{
if (entities[i] is not Line firstLine)
{
i++;
continue;
}
// Collect consecutive lines on the same layer
var runStart = i;
var layer = firstLine.Layer;
while (i < entities.Count && entities[i] is Line line && line.Layer == layer)
i++;
var runEnd = i - 1;
// Try to find arc candidates within this run
FindCandidatesInRun(entities, runStart, runEnd, candidates);
}
return candidates;
}
public Shape Apply(Shape shape, List<ArcCandidate> candidates)
@@ -31,6 +54,140 @@ public class GeometrySimplifier
throw new NotImplementedException();
}
private void FindCandidatesInRun(List<Entity> entities, int runStart, int runEnd, List<ArcCandidate> candidates)
{
var j = runStart;
while (j <= runEnd - MinLines + 1)
{
// Start with MinLines lines
var k = j + MinLines - 1;
var points = CollectPoints(entities, j, k);
var (center, radius) = FitCircle(points);
if (!center.IsValid() || MaxDeviation(points, center, radius) > Tolerance)
{
j++;
continue;
}
// Extend as far as possible
var prevCenter = center;
var prevRadius = radius;
var prevMaxDev = MaxDeviation(points, center, radius);
while (k + 1 <= runEnd)
{
k++;
points = CollectPoints(entities, j, k);
var (newCenter, newRadius) = FitCircle(points);
if (!newCenter.IsValid())
{
k--;
break;
}
var newMaxDev = MaxDeviation(points, newCenter, newRadius);
if (newMaxDev > Tolerance)
{
k--;
break;
}
prevCenter = newCenter;
prevRadius = newRadius;
prevMaxDev = newMaxDev;
}
// Build the candidate
var finalPoints = CollectPoints(entities, j, k);
var arc = BuildArc(prevCenter, prevRadius, finalPoints, entities[j]);
var bbox = ComputeBoundingBox(finalPoints);
candidates.Add(new ArcCandidate
{
StartIndex = j,
EndIndex = k,
FittedArc = arc,
MaxDeviation = prevMaxDev,
BoundingBox = bbox,
});
j = k + 1;
}
}
private static List<Vector> CollectPoints(List<Entity> entities, int start, int end)
{
var points = new List<Vector>();
points.Add(((Line)entities[start]).StartPoint);
for (var i = start; i <= end; i++)
points.Add(((Line)entities[i]).EndPoint);
return points;
}
private static double MaxDeviation(List<Vector> points, Vector center, double radius)
{
var maxDev = 0.0;
for (var i = 0; i < points.Count; i++)
{
var dev = System.Math.Abs(points[i].DistanceTo(center) - radius);
if (dev > maxDev)
maxDev = dev;
}
return maxDev;
}
private static Arc BuildArc(Vector center, double radius, List<Vector> points, Entity sourceEntity)
{
var firstPoint = points[0];
var lastPoint = points[^1];
var startAngle = System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X);
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
// Determine direction by summing signed angular changes
var totalAngle = 0.0;
for (var i = 0; i < points.Count - 1; i++)
{
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
var da = a2 - a1;
while (da > System.Math.PI) da -= Angle.TwoPI;
while (da < -System.Math.PI) da += Angle.TwoPI;
totalAngle += da;
}
var isReversed = totalAngle < 0;
// Normalize angles to [0, 2pi)
if (startAngle < 0) startAngle += Angle.TwoPI;
if (endAngle < 0) endAngle += Angle.TwoPI;
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
arc.Layer = sourceEntity.Layer;
arc.Color = sourceEntity.Color;
return arc;
}
private static Box ComputeBoundingBox(List<Vector> points)
{
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
for (var i = 0; i < points.Count; i++)
{
if (points[i].X < minX) minX = points[i].X;
if (points[i].Y < minY) minY = points[i].Y;
if (points[i].X > maxX) maxX = points[i].X;
if (points[i].Y > maxY) maxY = points[i].Y;
}
return new Box(minX, minY, maxX - minX, maxY - minY);
}
internal static (Vector center, double radius) FitCircle(List<Vector> points)
{
var n = points.Count;