Files
OpenNest/OpenNest.Engine/PartClassifier.cs
T
ajandClaude Opus 5.5 1b5e1b14a6 fix: leave etch/scribe marks out of nesting geometry
Every nesting-geometry consumer filtered only rapids, so scribe/etch
moves counted as part material. An etch tick that ends a hair outside
the outline (PEP bend ticks start on the notch edge) made the part
"open geometry leaving the material region": the job validator threw
and every built-in engine plus Gpt6Astra crashed on real PEP jobs
(PT75, drawing 4980 A01 PT77). Marks are only on the surface, so they
should never affect placement, collision, area, or validation.

- SpecialLayers.IsMaterial excludes Rapid and Scribe; used by drawing
  area, canonical angle, part collision, PartGeometry, plate perimeter,
  best-fit/pair evaluation, rotation analysis, GPU evaluators, and both
  validators. Timing, display, splitting and posts still see marks.
- ConvertGeometry also maps the saved SCRIBE layer name to Scribe, so
  programs rebuilt from stored entities keep their marks.
- NestReader repairs older files (e.g. PepNestExport output) whose
  programs saved etch as cut moves while source entities kept SCRIBE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 06:44:29 -04:00

114 lines
3.6 KiB
C#

using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
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 => SpecialLayers.IsMaterial(e.Layer));
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);
// Share the single angle formula with CanonicalAngle (no duplicate MBR compute).
result.PrimaryAngle = CanonicalAngle.FromMbr(mbr);
// 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;
}
}
}