feat: add ArcCandidate and Kasa circle fitting

Foundation for the geometry simplifier that will replace consecutive line
segments with fitted arcs. Adds ArcCandidate data class, GeometrySimplifier
with stub Analyze/Apply methods, and FitCircle using the Kasa algebraic
least-squares method. Also adds InternalsVisibleTo for OpenNest.Tests on
OpenNest.Core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 23:22:05 -04:00
parent 12173204d1
commit bbc02f6f3f
3 changed files with 139 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry;
public class ArcCandidate
{
public int ShapeIndex { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int LineCount => EndIndex - StartIndex + 1;
public Arc FittedArc { get; set; }
public double MaxDeviation { get; set; }
public Box BoundingBox { get; set; }
public bool IsSelected { get; set; } = true;
}
public class GeometrySimplifier
{
public double Tolerance { get; set; } = 0.005;
public int MinLines { get; set; } = 3;
public List<ArcCandidate> Analyze(Shape shape)
{
throw new NotImplementedException();
}
public Shape Apply(Shape shape, List<ArcCandidate> candidates)
{
throw new NotImplementedException();
}
internal static (Vector center, double radius) FitCircle(List<Vector> points)
{
var n = points.Count;
if (n < 3)
return (Vector.Invalid, 0);
double sumX = 0, sumY = 0, sumX2 = 0, sumY2 = 0, sumXY = 0;
double sumXZ = 0, sumYZ = 0, sumZ = 0;
for (var i = 0; i < n; i++)
{
var x = points[i].X;
var y = points[i].Y;
var z = x * x + y * y;
sumX += x;
sumY += y;
sumX2 += x * x;
sumY2 += y * y;
sumXY += x * y;
sumXZ += x * z;
sumYZ += y * z;
sumZ += z;
}
// Solve: [sumX2 sumXY sumX] [A] [sumXZ]
// [sumXY sumY2 sumY] [B] = [sumYZ]
// [sumX sumY n ] [C] [sumZ ]
var det = sumX2 * (sumY2 * n - sumY * sumY)
- sumXY * (sumXY * n - sumY * sumX)
+ sumX * (sumXY * sumY - sumY2 * sumX);
if (System.Math.Abs(det) < 1e-10)
return (Vector.Invalid, 0);
var detA = sumXZ * (sumY2 * n - sumY * sumY)
- sumXY * (sumYZ * n - sumY * sumZ)
+ sumX * (sumYZ * sumY - sumY2 * sumZ);
var detB = sumX2 * (sumYZ * n - sumY * sumZ)
- sumXZ * (sumXY * n - sumY * sumX)
+ sumX * (sumXY * sumZ - sumYZ * sumX);
var detC = sumX2 * (sumY2 * sumZ - sumYZ * sumY)
- sumXY * (sumXY * sumZ - sumYZ * sumX)
+ sumXZ * (sumXY * sumY - sumY2 * sumX);
var a = detA / det;
var b = detB / det;
var c = detC / det;
var cx = a / 2.0;
var cy = b / 2.0;
var rSquared = cx * cx + cy * cy + c;
if (rSquared <= 0)
return (Vector.Invalid, 0);
return (new Vector(cx, cy), System.Math.Sqrt(rSquared));
}
}

View File

@@ -4,6 +4,9 @@
<RootNamespace>OpenNest</RootNamespace>
<AssemblyName>OpenNest.Core</AssemblyName>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="OpenNest.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Clipper2" Version="2.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />