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 Analyze(Shape shape) { var candidates = new List(); 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 candidates) { throw new NotImplementedException(); } private void FindCandidatesInRun(List entities, int runStart, int runEnd, List 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 CollectPoints(List entities, int start, int end) { var points = new List(); 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 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 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 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 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)); } }