diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs
index 5e3a1eb..c07586a 100644
--- a/OpenNest.Core/Plate.cs
+++ b/OpenNest.Core/Plate.cs
@@ -125,6 +125,58 @@ namespace OpenNest
}
}
+ ///
+ /// Builds a dictionary mapping each non-cut-off part to its perimeter entity.
+ /// Closed shapes use ShapeProfile; open contours fall back to ConvexHull.
+ ///
+ public static Dictionary BuildPerimeterCache(Plate plate)
+ {
+ var cache = new Dictionary();
+
+ foreach (var part in plate.Parts)
+ {
+ if (part.BaseDrawing.IsCutOff)
+ continue;
+
+ Geometry.Entity perimeter = null;
+ try
+ {
+ var entities = Converters.ConvertProgram.ToGeometry(part.Program)
+ .Where(e => e.Layer != SpecialLayers.Rapid)
+ .ToList();
+
+ if (entities.Count > 0)
+ {
+ var profile = new Geometry.ShapeProfile(entities);
+
+ if (profile.Perimeter.IsClosed())
+ {
+ perimeter = profile.Perimeter;
+ perimeter.Offset(part.Location);
+ }
+ else
+ {
+ var points = entities.CollectPoints();
+ if (points.Count >= 3)
+ {
+ var hull = Geometry.ConvexHull.Compute(points);
+ hull.Offset(part.Location);
+ perimeter = hull;
+ }
+ }
+ }
+ }
+ catch
+ {
+ perimeter = null;
+ }
+
+ cache[part] = perimeter;
+ }
+
+ return cache;
+ }
+
///
/// The number of times to cut the plate.
///
diff --git a/OpenNest.Tests/CutOffGeometryTests.cs b/OpenNest.Tests/CutOffGeometryTests.cs
index 5e1315f..a16a1e9 100644
--- a/OpenNest.Tests/CutOffGeometryTests.cs
+++ b/OpenNest.Tests/CutOffGeometryTests.cs
@@ -336,6 +336,48 @@ public class CutOffGeometryTests
Assert.Equal(8, points.Count);
}
+ [Fact]
+ public void PlatePerimeterCache_ReturnsOneEntryPerPart()
+ {
+ var plate = new Plate(100, 100);
+ plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
+ plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
+ plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));
+
+ var cache = Plate.BuildPerimeterCache(plate);
+ Assert.Equal(3, cache.Count);
+ }
+
+ [Fact]
+ public void PlatePerimeterCache_SkipsCutOffParts()
+ {
+ var plate = new Plate(100, 100);
+ plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
+ plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));
+
+ var cache = Plate.BuildPerimeterCache(plate);
+ Assert.Single(cache);
+ }
+
+ [Fact]
+ public void PlatePerimeterCache_OpenContourUsesConvexHull()
+ {
+ var pgm = new Program();
+ pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
+ pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
+ pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
+
+ var plate = new Plate(100, 100);
+ plate.Parts.Add(new Part(new Drawing("open", pgm)));
+
+ var cache = Plate.BuildPerimeterCache(plate);
+ Assert.Single(cache);
+
+ var part = plate.Parts[0];
+ Assert.True(cache.ContainsKey(part));
+ Assert.NotNull(cache[part]);
+ }
+
[Fact]
public void ShapeProfile_SelectsLargestShapeAsPerimeter()
{