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>
149 lines
5.4 KiB
C#
149 lines
5.4 KiB
C#
using Clipper2Lib;
|
|
|
|
namespace OpenNest.Geometry
|
|
{
|
|
/// <summary>
|
|
/// Computes the Inner-Fit Polygon (IFP) — the feasible region where a part's
|
|
/// reference point can be placed so the part stays entirely within the plate boundary.
|
|
/// For a rectangular plate, the IFP is the plate shrunk by the part's bounding dimensions.
|
|
/// </summary>
|
|
public static class InnerFitPolygon
|
|
{
|
|
/// <summary>
|
|
/// Computes the IFP for placing a part polygon inside a rectangular work area.
|
|
/// The result is a polygon representing all valid reference point positions.
|
|
/// </summary>
|
|
public static Polygon Compute(Box workArea, Polygon partPolygon)
|
|
{
|
|
// Get the part's bounding box relative to its reference point (origin).
|
|
var verts = partPolygon.Vertices;
|
|
|
|
if (verts.Count < 3)
|
|
return new Polygon();
|
|
|
|
var minX = verts[0].X;
|
|
var maxX = verts[0].X;
|
|
var minY = verts[0].Y;
|
|
var maxY = verts[0].Y;
|
|
|
|
for (var i = 1; i < verts.Count; i++)
|
|
{
|
|
if (verts[i].X < minX) minX = verts[i].X;
|
|
if (verts[i].X > maxX) maxX = verts[i].X;
|
|
if (verts[i].Y < minY) minY = verts[i].Y;
|
|
if (verts[i].Y > maxY) maxY = verts[i].Y;
|
|
}
|
|
|
|
// The IFP is the work area shrunk inward by the part's extent in each direction.
|
|
// The reference point can range from (workArea.Left - minX) to (workArea.Right - maxX)
|
|
// and (workArea.Bottom - minY) to (workArea.Top - maxY).
|
|
var ifpLeft = workArea.X - minX;
|
|
var ifpRight = workArea.Right - maxX;
|
|
var ifpBottom = workArea.Y - minY;
|
|
var ifpTop = workArea.Top - maxY;
|
|
|
|
// If the part doesn't fit, return an empty polygon.
|
|
if (ifpRight < ifpLeft || ifpTop < ifpBottom)
|
|
return new Polygon();
|
|
|
|
var result = new Polygon();
|
|
result.Vertices.Add(new Vector(ifpLeft, ifpBottom));
|
|
result.Vertices.Add(new Vector(ifpRight, ifpBottom));
|
|
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
|
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
|
result.Close();
|
|
result.UpdateBounds();
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the feasible region for placing a part given already-placed parts.
|
|
/// FeasibleRegion = IFP(plate, part) - union(NFP(placed_i, part))
|
|
/// Returns the polygon representing valid placement positions, or an empty
|
|
/// polygon if no valid position exists.
|
|
/// </summary>
|
|
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
|
{
|
|
if (ifp.Vertices.Count < 3)
|
|
return new Polygon();
|
|
|
|
if (nfpPaths == null || nfpPaths.Count == 0)
|
|
return ifp;
|
|
|
|
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
|
var ifpPaths = new PathsD { ifpPath };
|
|
|
|
// 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();
|
|
|
|
// Find the polygon with the bottom-left-most point.
|
|
// This ensures we pick the correct region for placement.
|
|
PathD bestPath = null;
|
|
var bestY = double.MaxValue;
|
|
var bestX = double.MaxValue;
|
|
|
|
foreach (var path in feasible)
|
|
{
|
|
foreach (var pt in path)
|
|
{
|
|
if (pt.y < bestY || (pt.y == bestY && pt.x < bestX))
|
|
{
|
|
bestY = pt.y;
|
|
bestX = pt.x;
|
|
bestPath = path;
|
|
}
|
|
}
|
|
}
|
|
|
|
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>
|
|
/// Finds the bottom-left-most point on a polygon boundary.
|
|
/// "Bottom-left" means: minimize Y first, then minimize X.
|
|
/// Returns Vector.Invalid if the polygon has no vertices.
|
|
/// </summary>
|
|
public static Vector FindBottomLeftPoint(Polygon polygon)
|
|
{
|
|
if (polygon.Vertices.Count == 0)
|
|
return Vector.Invalid;
|
|
|
|
var best = polygon.Vertices[0];
|
|
|
|
for (var i = 1; i < polygon.Vertices.Count; i++)
|
|
{
|
|
var v = polygon.Vertices[i];
|
|
|
|
if (v.Y < best.Y || (v.Y == best.Y && v.X < best.X))
|
|
best = v;
|
|
}
|
|
|
|
return best;
|
|
}
|
|
}
|
|
}
|