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) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 14:42:43 -04:00
parent 2ed02c2dae
commit facd07d7de
3 changed files with 40 additions and 25 deletions

View File

@@ -74,6 +74,16 @@ namespace OpenNest.Geometry
Location += voffset; 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 public double Left
{ {
get { return X; } get { return X; }

View File

@@ -52,6 +52,7 @@ namespace OpenNest.Geometry
result.Vertices.Add(new Vector(ifpRight, ifpTop)); result.Vertices.Add(new Vector(ifpRight, ifpTop));
result.Vertices.Add(new Vector(ifpLeft, ifpTop)); result.Vertices.Add(new Vector(ifpLeft, ifpTop));
result.Close(); result.Close();
result.UpdateBounds();
return result; return result;
} }
@@ -62,36 +63,20 @@ namespace OpenNest.Geometry
/// Returns the polygon representing valid placement positions, or an empty /// Returns the polygon representing valid placement positions, or an empty
/// polygon if no valid position exists. /// polygon if no valid position exists.
/// </summary> /// </summary>
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps) public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
{ {
if (ifp.Vertices.Count < 3) if (ifp.Vertices.Count < 3)
return new Polygon(); return new Polygon();
if (nfps == null || nfps.Length == 0) if (nfpPaths == null || nfpPaths.Count == 0)
return ifp; return ifp;
var ifpPath = NoFitPolygon.ToClipperPath(ifp); var ifpPath = NoFitPolygon.ToClipperPath(ifp);
var ifpPaths = new PathsD { ifpPath }; var ifpPaths = new PathsD { ifpPath };
// Union all NFPs. // Subtract the NFPs from the IFP.
var nfpPaths = new PathsD(); // Clipper2 handles the implicit union of the clip paths.
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
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);
if (feasible.Count == 0) if (feasible.Count == 0)
return new Polygon(); return new Polygon();
@@ -118,6 +103,25 @@ namespace OpenNest.Geometry
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon(); return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
} }
/// <summary>
/// Computes the feasible region for placing a part given already-placed parts.
/// (Legacy overload for backward compatibility).
/// </summary>
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);
}
/// <summary> /// <summary>
/// Finds the bottom-left-most point on a polygon boundary. /// Finds the bottom-left-most point on a polygon boundary.
/// "Bottom-left" means: minimize Y first, then minimize X. /// "Bottom-left" means: minimize Y first, then minimize X.

View File

@@ -250,9 +250,9 @@ namespace OpenNest.Geometry
} }
/// <summary> /// <summary>
/// Converts an OpenNest Polygon to a Clipper2 PathD. /// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
/// </summary> /// </summary>
internal static PathD ToClipperPath(Polygon polygon) public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
{ {
var path = new PathD(); var path = new PathD();
var verts = polygon.Vertices; var verts = polygon.Vertices;
@@ -263,7 +263,7 @@ namespace OpenNest.Geometry
n--; n--;
for (var i = 0; i < n; i++) 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; return path;
} }
@@ -271,7 +271,7 @@ namespace OpenNest.Geometry
/// <summary> /// <summary>
/// Converts a Clipper2 PathD to an OpenNest Polygon. /// Converts a Clipper2 PathD to an OpenNest Polygon.
/// </summary> /// </summary>
internal static Polygon FromClipperPath(PathD path) public static Polygon FromClipperPath(PathD path)
{ {
var polygon = new Polygon(); var polygon = new Polygon();
@@ -279,6 +279,7 @@ namespace OpenNest.Geometry
polygon.Vertices.Add(new Vector(pt.x, pt.y)); polygon.Vertices.Add(new Vector(pt.x, pt.y));
polygon.Close(); polygon.Close();
polygon.UpdateBounds();
return polygon; return polygon;
} }
} }