Files
OpenNest/OpenNest.Engine/PartClassifier.cs
AJ Isaacs 05037bc928 feat: wire PartClassifier into engine and update angle selection
Replace RotationAnalysis.FindBestRotation with PartClassifier.Classify in
RunPipeline, propagate ClassificationResult through BuildAngles signatures and
FillContext.PartType, and rewrite AngleCandidateBuilder to dispatch on part type
(Circle=1 angle, Rectangle=2, Irregular=full sweep).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:19:20 -04:00

105 lines
3.5 KiB
C#

using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine
{
public enum PartType { Rectangle, Circle, Irregular }
public struct ClassificationResult
{
public PartType Type;
public double Rectangularity;
public double Circularity;
public double PerimeterRatio;
public double PrimaryAngle;
}
public static class PartClassifier
{
public const double RectangularityThreshold = 0.92;
public const double PerimeterRatioThreshold = 0.85;
public const double CircularityThreshold = 0.95;
public static ClassificationResult Classify(Drawing drawing)
{
var result = new ClassificationResult { Type = PartType.Irregular };
var entities = ConvertProgram.ToGeometry(drawing.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = ShapeBuilder.GetShapes(entities);
if (shapes.Count == 0)
return result;
// Find the largest shape (outer perimeter).
var perimeter = shapes[0];
var perimeterArea = perimeter.Area();
for (var i = 1; i < shapes.Count; i++)
{
var area = shapes[i].Area();
if (area > perimeterArea)
{
perimeter = shapes[i];
perimeterArea = area;
}
}
// Convert to polygon for hull/MBR computation.
var polygon = perimeter.ToPolygonWithTolerance(0.1);
if (polygon == null || polygon.Vertices.Count < 3)
return result;
// Compute convex hull.
var hull = ConvexHull.Compute(polygon.Vertices);
var hullArea = hull.Area();
// Compute MBR via rotating calipers.
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
var mbrArea = mbr.Area;
var mbrPerimeter = 2 * (mbr.Width + mbr.Height);
// Store primary angle (negated to align MBR with axes, same as RotationAnalysis).
result.PrimaryAngle = -mbr.Angle;
// Drawing perimeter for circularity and perimeter ratio.
var drawingPerimeter = polygon.Perimeter();
// Circularity: 4*PI*area / perimeter^2. Circles ~ 1.0.
if (drawingPerimeter > Tolerance.Epsilon)
result.Circularity = 4 * System.Math.PI * perimeterArea / (drawingPerimeter * drawingPerimeter);
// Check circle first (rotationally invariant).
if (result.Circularity >= CircularityThreshold)
{
result.Type = PartType.Circle;
return result;
}
// Rectangularity: hull area / MBR area.
if (mbrArea > Tolerance.Epsilon)
result.Rectangularity = hullArea / mbrArea;
// Perimeter ratio: MBR perimeter / drawing perimeter.
if (drawingPerimeter > Tolerance.Epsilon)
result.PerimeterRatio = mbrPerimeter / drawingPerimeter;
// Rectangle: both metrics pass thresholds.
if (result.Rectangularity >= RectangularityThreshold
&& result.PerimeterRatio >= PerimeterRatioThreshold)
{
result.Type = PartType.Rectangle;
return result;
}
result.Type = PartType.Irregular;
return result;
}
}
}