From facd07d7de4cb8771eca19f8372e4e1254156d04 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 20 Mar 2026 14:42:43 -0400 Subject: [PATCH] feat: add Box.Translate and improve NFP/IFP geometry APIs Add immutable Translate methods to Box. Make NoFitPolygon ToClipperPath/FromClipperPath public with optional offset parameter. Refactor InnerFitPolygon.ComputeFeasibleRegion to accept PathsD directly, letting Clipper2 handle implicit union. Add UpdateBounds calls after polygon construction. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/Box.cs | 10 +++++ OpenNest.Core/Geometry/InnerFitPolygon.cs | 46 ++++++++++++----------- OpenNest.Core/Geometry/NoFitPolygon.cs | 9 +++-- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/OpenNest.Core/Geometry/Box.cs b/OpenNest.Core/Geometry/Box.cs index 6d4bcdc..7145bb7 100644 --- a/OpenNest.Core/Geometry/Box.cs +++ b/OpenNest.Core/Geometry/Box.cs @@ -74,6 +74,16 @@ namespace OpenNest.Geometry Location += voffset; } + public Box Translate(double x, double y) + { + return new Box(X + x, Y + y, Width, Length); + } + + public Box Translate(Vector offset) + { + return new Box(X + offset.X, Y + offset.Y, Width, Length); + } + public double Left { get { return X; } diff --git a/OpenNest.Core/Geometry/InnerFitPolygon.cs b/OpenNest.Core/Geometry/InnerFitPolygon.cs index bfa09d4..b4c9f68 100644 --- a/OpenNest.Core/Geometry/InnerFitPolygon.cs +++ b/OpenNest.Core/Geometry/InnerFitPolygon.cs @@ -52,6 +52,7 @@ namespace OpenNest.Geometry result.Vertices.Add(new Vector(ifpRight, ifpTop)); result.Vertices.Add(new Vector(ifpLeft, ifpTop)); result.Close(); + result.UpdateBounds(); return result; } @@ -62,36 +63,20 @@ namespace OpenNest.Geometry /// Returns the polygon representing valid placement positions, or an empty /// polygon if no valid position exists. /// - public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps) + public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths) { if (ifp.Vertices.Count < 3) return new Polygon(); - if (nfps == null || nfps.Length == 0) + if (nfpPaths == null || nfpPaths.Count == 0) return ifp; var ifpPath = NoFitPolygon.ToClipperPath(ifp); var ifpPaths = new PathsD { ifpPath }; - // Union all NFPs. - var nfpPaths = new PathsD(); - - foreach (var nfp in nfps) - { - if (nfp.Vertices.Count >= 3) - { - var path = NoFitPolygon.ToClipperPath(nfp); - nfpPaths.Add(path); - } - } - - if (nfpPaths.Count == 0) - return ifp; - - var nfpUnion = Clipper.Union(nfpPaths, FillRule.NonZero); - - // Subtract the NFP union from the IFP. - var feasible = Clipper.Difference(ifpPaths, nfpUnion, FillRule.NonZero); + // Subtract the NFPs from the IFP. + // Clipper2 handles the implicit union of the clip paths. + var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero); if (feasible.Count == 0) return new Polygon(); @@ -118,6 +103,25 @@ namespace OpenNest.Geometry return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon(); } + /// + /// Computes the feasible region for placing a part given already-placed parts. + /// (Legacy overload for backward compatibility). + /// + public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps) + { + if (nfps == null || nfps.Length == 0) + return ifp; + + var nfpPaths = new PathsD(nfps.Length); + foreach (var nfp in nfps) + { + if (nfp.Vertices.Count >= 3) + nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp)); + } + + return ComputeFeasibleRegion(ifp, nfpPaths); + } + /// /// Finds the bottom-left-most point on a polygon boundary. /// "Bottom-left" means: minimize Y first, then minimize X. diff --git a/OpenNest.Core/Geometry/NoFitPolygon.cs b/OpenNest.Core/Geometry/NoFitPolygon.cs index 3e89222..4749517 100644 --- a/OpenNest.Core/Geometry/NoFitPolygon.cs +++ b/OpenNest.Core/Geometry/NoFitPolygon.cs @@ -250,9 +250,9 @@ namespace OpenNest.Geometry } /// - /// Converts an OpenNest Polygon to a Clipper2 PathD. + /// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset. /// - internal static PathD ToClipperPath(Polygon polygon) + public static PathD ToClipperPath(Polygon polygon, Vector offset = default) { var path = new PathD(); var verts = polygon.Vertices; @@ -263,7 +263,7 @@ namespace OpenNest.Geometry n--; for (var i = 0; i < n; i++) - path.Add(new PointD(verts[i].X, verts[i].Y)); + path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y)); return path; } @@ -271,7 +271,7 @@ namespace OpenNest.Geometry /// /// Converts a Clipper2 PathD to an OpenNest Polygon. /// - internal static Polygon FromClipperPath(PathD path) + public static Polygon FromClipperPath(PathD path) { var polygon = new Polygon(); @@ -279,6 +279,7 @@ namespace OpenNest.Geometry polygon.Vertices.Add(new Vector(pt.x, pt.y)); polygon.Close(); + polygon.UpdateBounds(); return polygon; } }