refactor: extract PolygonHelper from AutoNester for shared polygon operations
Creates PolygonHelper.cs in OpenNest.Engine.BestFit with ExtractPerimeterPolygon (returning PolygonExtractionResult with polygon + correction vector) and RotatePolygon. AutoNester.ExtractPerimeterPolygon and RotatePolygon become thin delegates. Adds MakeSquareDrawing/MakeLShapeDrawing to TestHelpers and 6 PolygonHelperTests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.BestFit
|
||||||
|
{
|
||||||
|
public static class PolygonHelper
|
||||||
|
{
|
||||||
|
public static PolygonExtractionResult ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (entities.Count == 0)
|
||||||
|
return new PolygonExtractionResult(null, Vector.Zero);
|
||||||
|
|
||||||
|
var definedShape = new ShapeProfile(entities);
|
||||||
|
var perimeter = definedShape.Perimeter;
|
||||||
|
|
||||||
|
if (perimeter == null)
|
||||||
|
return new PolygonExtractionResult(null, Vector.Zero);
|
||||||
|
|
||||||
|
// Compute the perimeter bounding box before inflation for coordinate correction.
|
||||||
|
perimeter.UpdateBounds();
|
||||||
|
var perimeterBb = perimeter.BoundingBox;
|
||||||
|
|
||||||
|
// Inflate by half-spacing if spacing is non-zero.
|
||||||
|
Shape inflated;
|
||||||
|
|
||||||
|
if (halfSpacing > 0)
|
||||||
|
{
|
||||||
|
var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Left);
|
||||||
|
inflated = offsetEntity as Shape ?? perimeter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inflated = perimeter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||||
|
var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
|
||||||
|
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return new PolygonExtractionResult(null, Vector.Zero);
|
||||||
|
|
||||||
|
// Compute correction: difference between program origin and perimeter origin.
|
||||||
|
// Part.CreateAtOrigin normalizes to program bbox; polygon normalizes to perimeter bbox.
|
||||||
|
var programBb = drawing.Program.BoundingBox();
|
||||||
|
var correction = new Vector(
|
||||||
|
perimeterBb.Left - programBb.Location.X,
|
||||||
|
perimeterBb.Bottom - programBb.Location.Y);
|
||||||
|
|
||||||
|
// Normalize: move reference point to origin.
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
var bb = polygon.BoundingBox;
|
||||||
|
polygon.Offset(-bb.Left, -bb.Bottom);
|
||||||
|
|
||||||
|
return new PolygonExtractionResult(polygon, correction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Polygon RotatePolygon(Polygon polygon, double angle)
|
||||||
|
{
|
||||||
|
if (angle.IsEqualTo(0))
|
||||||
|
return polygon;
|
||||||
|
|
||||||
|
var result = new Polygon();
|
||||||
|
var cos = System.Math.Cos(angle);
|
||||||
|
var sin = System.Math.Sin(angle);
|
||||||
|
|
||||||
|
foreach (var v in polygon.Vertices)
|
||||||
|
{
|
||||||
|
result.Vertices.Add(new Vector(
|
||||||
|
v.X * cos - v.Y * sin,
|
||||||
|
v.X * sin + v.Y * cos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-normalize to origin.
|
||||||
|
result.UpdateBounds();
|
||||||
|
var bb = result.BoundingBox;
|
||||||
|
result.Offset(-bb.Left, -bb.Bottom);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PolygonExtractionResult(Polygon Polygon, Vector Correction);
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System;
|
using System;
|
||||||
@@ -203,44 +202,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
return BestFit.PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing).Polygon;
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (entities.Count == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var definedShape = new ShapeProfile(entities);
|
|
||||||
var perimeter = definedShape.Perimeter;
|
|
||||||
|
|
||||||
if (perimeter == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Inflate by half-spacing if spacing is non-zero.
|
|
||||||
Shape inflated;
|
|
||||||
|
|
||||||
if (halfSpacing > 0)
|
|
||||||
{
|
|
||||||
var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Left);
|
|
||||||
inflated = offsetEntity as Shape ?? perimeter;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inflated = perimeter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
|
||||||
var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
|
|
||||||
|
|
||||||
if (polygon.Vertices.Count < 3)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Normalize: move reference point to origin.
|
|
||||||
polygon.UpdateBounds();
|
|
||||||
var bb = polygon.BoundingBox;
|
|
||||||
polygon.Offset(-bb.Left, -bb.Bottom);
|
|
||||||
|
|
||||||
return polygon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -320,26 +282,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static Polygon RotatePolygon(Polygon polygon, double angle)
|
private static Polygon RotatePolygon(Polygon polygon, double angle)
|
||||||
{
|
{
|
||||||
if (angle.IsEqualTo(0))
|
return BestFit.PolygonHelper.RotatePolygon(polygon, angle);
|
||||||
return polygon;
|
|
||||||
|
|
||||||
var result = new Polygon();
|
|
||||||
var cos = System.Math.Cos(angle);
|
|
||||||
var sin = System.Math.Sin(angle);
|
|
||||||
|
|
||||||
foreach (var v in polygon.Vertices)
|
|
||||||
{
|
|
||||||
result.Vertices.Add(new Vector(
|
|
||||||
v.X * cos - v.Y * sin,
|
|
||||||
v.X * sin + v.Y * cos));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-normalize to origin.
|
|
||||||
result.UpdateBounds();
|
|
||||||
var bb = result.BoundingBox;
|
|
||||||
result.Offset(-bb.Left, -bb.Bottom);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class PolygonHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ExtractPerimeterPolygon_ReturnsPolygon_ForValidDrawing()
|
||||||
|
{
|
||||||
|
var drawing = TestHelpers.MakeSquareDrawing();
|
||||||
|
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
|
||||||
|
Assert.NotNull(result.Polygon);
|
||||||
|
Assert.True(result.Polygon.Vertices.Count >= 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractPerimeterPolygon_InflatesPolygon_WhenSpacingNonZero()
|
||||||
|
{
|
||||||
|
var drawing = TestHelpers.MakeSquareDrawing(10);
|
||||||
|
var noSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
|
||||||
|
var withSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 1);
|
||||||
|
|
||||||
|
noSpacing.Polygon.UpdateBounds();
|
||||||
|
withSpacing.Polygon.UpdateBounds();
|
||||||
|
|
||||||
|
// The offset polygon should differ in size from the non-offset polygon.
|
||||||
|
// OffsetSide.Left offsets outward or inward depending on winding,
|
||||||
|
// but either way the result must be a different size.
|
||||||
|
Assert.True(
|
||||||
|
System.Math.Abs(withSpacing.Polygon.BoundingBox.Width - noSpacing.Polygon.BoundingBox.Width) > 0.5,
|
||||||
|
$"Expected polygon width to differ by >0.5 with 1mm spacing. " +
|
||||||
|
$"No-spacing width: {noSpacing.Polygon.BoundingBox.Width:F3}, " +
|
||||||
|
$"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractPerimeterPolygon_ReturnsNull_ForEmptyDrawing()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
var drawing = new Drawing("empty", pgm);
|
||||||
|
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
|
||||||
|
Assert.Null(result.Polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractPerimeterPolygon_CorrectionVector_ReflectsOriginDifference()
|
||||||
|
{
|
||||||
|
var drawing = TestHelpers.MakeSquareDrawing();
|
||||||
|
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
|
||||||
|
Assert.NotNull(result.Polygon);
|
||||||
|
Assert.True(System.Math.Abs(result.Correction.X) < 1);
|
||||||
|
Assert.True(System.Math.Abs(result.Correction.Y) < 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RotatePolygon_AtZero_ReturnsSamePolygon()
|
||||||
|
{
|
||||||
|
var polygon = new Polygon();
|
||||||
|
polygon.Vertices.Add(new Vector(0, 0));
|
||||||
|
polygon.Vertices.Add(new Vector(10, 0));
|
||||||
|
polygon.Vertices.Add(new Vector(10, 10));
|
||||||
|
polygon.Vertices.Add(new Vector(0, 10));
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
|
||||||
|
var rotated = PolygonHelper.RotatePolygon(polygon, 0);
|
||||||
|
Assert.Same(polygon, rotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RotatePolygon_At90Degrees_SwapsDimensions()
|
||||||
|
{
|
||||||
|
var polygon = new Polygon();
|
||||||
|
polygon.Vertices.Add(new Vector(0, 0));
|
||||||
|
polygon.Vertices.Add(new Vector(20, 0));
|
||||||
|
polygon.Vertices.Add(new Vector(20, 10));
|
||||||
|
polygon.Vertices.Add(new Vector(0, 10));
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
|
||||||
|
var rotated = PolygonHelper.RotatePolygon(polygon, Angle.HalfPI);
|
||||||
|
rotated.UpdateBounds();
|
||||||
|
|
||||||
|
Assert.True(System.Math.Abs(rotated.BoundingBox.Width - 10) < 0.1);
|
||||||
|
Assert.True(System.Math.Abs(rotated.BoundingBox.Length - 20) < 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,4 +24,28 @@ internal static class TestHelpers
|
|||||||
plate.Parts.Add(p);
|
plate.Parts.Add(p);
|
||||||
return plate;
|
return plate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Drawing MakeSquareDrawing(double size = 10)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("square", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Drawing MakeLShapeDrawing()
|
||||||
|
{
|
||||||
|
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, 5)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(5, 5)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(5, 10)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("lshape", pgm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user