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:
@@ -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;
|
||||
|
||||
@@ -40,4 +40,71 @@ public class GeometrySimplifierTests
|
||||
|
||||
Assert.False(fitCenter.IsValid());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_LinesFromSemicircle_FindsOneCandidate()
|
||||
{
|
||||
// Create 20 lines approximating a semicircle of radius 10
|
||||
var arc = new Arc(new Vector(0, 0), 10, 0, System.Math.PI, false);
|
||||
var points = arc.ToPoints(20);
|
||||
var shape = new Shape();
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
shape.Entities.Add(new Line(points[i], points[i + 1]));
|
||||
|
||||
var simplifier = new GeometrySimplifier { Tolerance = 0.1 };
|
||||
var candidates = simplifier.Analyze(shape);
|
||||
|
||||
Assert.Single(candidates);
|
||||
Assert.Equal(0, candidates[0].StartIndex);
|
||||
Assert.Equal(19, candidates[0].EndIndex);
|
||||
Assert.Equal(20, candidates[0].LineCount);
|
||||
Assert.InRange(candidates[0].FittedArc.Radius, 9.5, 10.5);
|
||||
Assert.True(candidates[0].MaxDeviation <= 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_TooFewLines_ReturnsNoCandidates()
|
||||
{
|
||||
// Only 2 consecutive lines — below MinLines threshold
|
||||
var shape = new Shape();
|
||||
shape.Entities.Add(new Line(new Vector(0, 0), new Vector(1, 1)));
|
||||
shape.Entities.Add(new Line(new Vector(1, 1), new Vector(2, 0)));
|
||||
|
||||
var simplifier = new GeometrySimplifier { Tolerance = 0.1, MinLines = 3 };
|
||||
var candidates = simplifier.Analyze(shape);
|
||||
|
||||
Assert.Empty(candidates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MixedEntitiesWithArc_OnlyAnalyzesLines()
|
||||
{
|
||||
// Line, Line, Line, Arc, Line, Line, Line — should find candidates only in line runs
|
||||
var shape = new Shape();
|
||||
// First run: 5 lines on a curve
|
||||
var arc1 = new Arc(new Vector(0, 0), 10, 0, System.Math.PI / 2, false);
|
||||
var pts1 = arc1.ToPoints(5);
|
||||
for (var i = 0; i < pts1.Count - 1; i++)
|
||||
shape.Entities.Add(new Line(pts1[i], pts1[i + 1]));
|
||||
|
||||
// An existing arc entity (breaks the run)
|
||||
shape.Entities.Add(new Arc(new Vector(20, 0), 5, 0, System.Math.PI, false));
|
||||
|
||||
// Second run: 4 lines on a different curve
|
||||
var arc2 = new Arc(new Vector(30, 0), 8, 0, System.Math.PI / 3, false);
|
||||
var pts2 = arc2.ToPoints(4);
|
||||
for (var i = 0; i < pts2.Count - 1; i++)
|
||||
shape.Entities.Add(new Line(pts2[i], pts2[i + 1]));
|
||||
|
||||
var simplifier = new GeometrySimplifier { Tolerance = 0.5, MinLines = 3 };
|
||||
var candidates = simplifier.Analyze(shape);
|
||||
|
||||
Assert.Equal(2, candidates.Count);
|
||||
// First candidate covers indices 0-4 (5 lines)
|
||||
Assert.Equal(0, candidates[0].StartIndex);
|
||||
Assert.Equal(4, candidates[0].EndIndex);
|
||||
// Second candidate covers indices 6-9 (4 lines, after the arc at index 5)
|
||||
Assert.Equal(6, candidates[1].StartIndex);
|
||||
Assert.Equal(9, candidates[1].EndIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user