feat: fill open area and optimize pattern rotation via convex hull

ActionClone.Fill() now computes the largest open rectangle from the
cursor position (trying both vertical and horizontal) and passes it
to the engine, so fills no longer overlap existing parts.

Pattern fills try all convex hull edge angles to find the rotation
that maximizes part count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 10:51:21 -05:00
parent 0b2100a661
commit 0573cb2f6d
2 changed files with 166 additions and 14 deletions

View File

@@ -62,22 +62,28 @@ namespace OpenNest
var workArea = Plate.WorkArea();
var engine = new FillLinear(workArea, Plate.PartSpacing);
var angles = FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles);
// Build a pattern from the group of parts.
var pattern = new Pattern();
foreach (var part in groupParts)
{
var clone = (Part)part.Clone();
clone.UpdateBounds();
pattern.Parts.Add(clone);
}
pattern.UpdateBounds();
if (best == null || best.Count == 0)
return false;
Plate.Parts.AddRange(best);
return true;
}
public bool Fill(NestItem item, Box workArea)
{
var bestRotation = FindBestRotation(item);
var engine = new FillLinear(workArea, Plate.PartSpacing);
// Try 4 configurations: 2 axes x 2 orientations (horizontal/vertical).
var configs = new[]
{
engine.Fill(pattern, NestDirection.Horizontal),
engine.Fill(pattern, NestDirection.Vertical)
engine.Fill(item.Drawing, bestRotation, NestDirection.Horizontal),
engine.Fill(item.Drawing, bestRotation, NestDirection.Vertical),
engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Horizontal),
engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical)
};
List<Part> best = null;
@@ -88,6 +94,25 @@ namespace OpenNest
best = config;
}
if (best == null || best.Count == 0)
return false;
if (item.Quantity > 0 && best.Count > item.Quantity)
best = best.Take(item.Quantity).ToList();
Plate.Parts.AddRange(best);
return true;
}
public bool Fill(List<Part> groupParts, Box workArea)
{
if (groupParts == null || groupParts.Count == 0)
return false;
var engine = new FillLinear(workArea, Plate.PartSpacing);
var angles = FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles);
if (best == null || best.Count == 0)
return false;
@@ -185,6 +210,97 @@ namespace OpenNest
return parts.Count > 0;
}
private List<double> FindHullEdgeAngles(List<Part> parts)
{
var points = new List<Vector>();
foreach (var part in parts)
{
var entities = ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = Helper.GetShapes(entities);
foreach (var shape in shapes)
{
var polygon = shape.ToPolygonWithTolerance(0.1);
foreach (var vertex in polygon.Vertices)
points.Add(vertex + part.Location);
}
}
if (points.Count < 3)
return new List<double> { 0 };
var hull = ConvexHull.Compute(points);
var vertices = hull.Vertices;
var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
var angles = new List<double> { 0 };
for (var i = 0; i < n; i++)
{
var next = (i + 1) % n;
var dx = vertices[next].X - vertices[i].X;
var dy = vertices[next].Y - vertices[i].Y;
if (dx * dx + dy * dy < Tolerance.Epsilon)
continue;
var angle = -System.Math.Atan2(dy, dx);
if (!angles.Any(a => a.IsEqualTo(angle)))
angles.Add(angle);
}
return angles;
}
private Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
{
var pattern = new Pattern();
var center = ((IEnumerable<IBoundable>)groupParts).GetBoundingBox().Center;
foreach (var part in groupParts)
{
var clone = (Part)part.Clone();
clone.UpdateBounds();
if (!angle.IsEqualTo(0))
clone.Rotate(angle, center);
pattern.Parts.Add(clone);
}
pattern.UpdateBounds();
return pattern;
}
private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles)
{
List<Part> best = null;
foreach (var angle in angles)
{
var pattern = BuildRotatedPattern(groupParts, angle);
if (pattern.Parts.Count == 0)
continue;
var h = engine.Fill(pattern, NestDirection.Horizontal);
var v = engine.Fill(pattern, NestDirection.Vertical);
if (best == null || h.Count > best.Count)
best = h;
if (best == null || v.Count > best.Count)
best = v;
}
return best;
}
private double FindBestRotation(NestItem item)
{
var entities = ConvertProgram.ToGeometry(item.Drawing.Program)

View File

@@ -106,6 +106,17 @@ namespace OpenNest.Actions
plateView.SelectedParts.AddRange(parts);
}
public override void ConnectEvents()
{
plateView.KeyDown += plateView_KeyDown;
plateView.MouseMove += plateView_MouseMove;
plateView.MouseDown += plateView_MouseDown;
plateView.Paint += plateView_Paint;
plateView.SelectedParts.Clear();
plateView.SelectedParts.AddRange(parts);
}
public override void DisconnectEvents()
{
plateView.KeyDown -= plateView_KeyDown;
@@ -159,9 +170,34 @@ namespace OpenNest.Actions
private void Fill()
{
var engine = new NestEngine(plateView.Plate);
var plate = plateView.Plate;
var engine = new NestEngine(plate);
var groupParts = parts.Select(p => p.BasePart).ToList();
engine.Fill(groupParts);
var bounds = plate.WorkArea();
if (plate.Parts.Count == 0)
{
engine.Fill(groupParts);
return;
}
var boxes = new List<Box>();
foreach (var part in plate.Parts)
boxes.Add(part.BoundingBox.Offset(plate.PartSpacing));
var pt = plateView.CurrentPoint;
var vertical = Helper.GetLargestBoxVertically(pt, bounds, boxes);
var horizontal = Helper.GetLargestBoxHorizontally(pt, bounds, boxes);
var bestArea = vertical;
if (horizontal.Area() > vertical.Area())
bestArea = horizontal;
if (bestArea == Box.Empty)
return;
engine.Fill(groupParts, bestArea);
}
}
}