Files
OpenNest/OpenNest.Engine/ML/FeatureExtractor.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

95 lines
3.4 KiB
C#

using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.ML
{
public class PartFeatures
{
// --- Geometric Features ---
public double Area { get; set; }
public double Convexity { get; set; } // Area / Convex Hull Area
public double AspectRatio { get; set; } // Width / Length
public double BoundingBoxFill { get; set; } // Area / (Width * Length)
public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2
public double PerimeterToAreaRatio { get; set; } // Perimeter / Area — spacing sensitivity
public int VertexCount { get; set; }
// --- Normalized Bitmask (32x32 = 1024 features) ---
public byte[] Bitmask { get; set; }
public override string ToString()
{
return $"{Area:F2},{Convexity:F4},{AspectRatio:F4},{BoundingBoxFill:F4},{Circularity:F4},{PerimeterToAreaRatio:F4},{VertexCount}";
}
}
public static class FeatureExtractor
{
public static PartFeatures Extract(Drawing drawing)
{
// Normalize to canonical frame so features are invariant to import orientation.
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
var entities = OpenNest
.Converters.ConvertProgram.ToGeometry(canonical.Program)
.Where(e => SpecialLayers.IsMaterial(e.Layer))
.ToList();
var profile = new ShapeProfile(entities);
var perimeter = profile.Perimeter;
if (perimeter == null)
return null;
var polygon = perimeter.ToPolygonWithTolerance(0.01);
polygon.UpdateBounds();
var bb = polygon.BoundingBox;
var hull = ConvexHull.Compute(polygon.Vertices);
var hullArea = hull.Area();
var features = new PartFeatures
{
Area = canonical.Area,
Convexity = canonical.Area / (hullArea > 0 ? hullArea : 1.0),
AspectRatio = bb.Length / (bb.Width > 0 ? bb.Width : 1.0),
BoundingBoxFill = canonical.Area / (bb.Area() > 0 ? bb.Area() : 1.0),
VertexCount = polygon.Vertices.Count,
Bitmask = GenerateBitmask(polygon, 32),
};
// Circularity = 4 * PI * Area / Perimeter^2
var perimeterLen = polygon.Perimeter();
features.Circularity =
(4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen);
features.PerimeterToAreaRatio = canonical.Area > 0 ? perimeterLen / canonical.Area : 0;
return features;
}
private static byte[] GenerateBitmask(Polygon polygon, int size)
{
var mask = new byte[size * size];
polygon.UpdateBounds();
var bb = polygon.BoundingBox;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
// Map grid coordinate (0..size) to bounding box coordinate
var px = bb.Left + (x + 0.5) * (bb.Length / size);
var py = bb.Bottom + (y + 0.5) * (bb.Width / size);
if (polygon.ContainsPoint(new Vector(px, py)))
{
mask[y * size + x] = 1;
}
}
}
return mask;
}
}
}