Files
OpenNest/OpenNest.Engine/NestEngine.cs
AJ Isaacs 3220306d3a feat: add reverse push directions for concave interlocking and cache best-fit results
Add PushDirection.Right and PushDirection.Up to RotationSlideStrategy so
parts can approach from all four directions. This discovers concave
interlocking arrangements (e.g. L-shaped parts nesting into each other's
cavities) that the original Left/Down-only slides could never reach.

Introduce BestFitCache so best-fit results are computed once at step size
0.25 and shared between the viewer and nesting engine. The GPU evaluator
factory is configured once at startup instead of being wired per call
site, and NestEngine.CreateEvaluator is removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:02:41 -04:00

275 lines
9.1 KiB
C#

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
using OpenNest.RectanglePacking;
namespace OpenNest
{
public class NestEngine
{
public NestEngine(Plate plate)
{
Plate = plate;
}
public Plate Plate { get; set; }
public NestDirection NestDirection { get; set; }
public bool Fill(NestItem item)
{
return Fill(item, Plate.WorkArea());
}
public bool Fill(List<Part> groupParts)
{
return Fill(groupParts, Plate.WorkArea());
}
public bool Fill(NestItem item, Box workArea)
{
var bestRotation = RotationAnalysis.FindBestRotation(item);
var engine = new FillLinear(workArea, Plate.PartSpacing);
var configs = new[]
{
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;
foreach (var config in configs)
{
if (IsBetterFill(config, best))
best = config;
}
Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
// Try rectangle best-fit (mixes orientations to fill remnant strips).
var rectResult = FillRectangleBestFit(item, workArea);
Debug.WriteLine($"[Fill(NestItem,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best))
best = rectResult;
// Try pair-based approach.
var pairResult = FillWithPairs(item, workArea);
Debug.WriteLine($"[Fill(NestItem,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}");
if (IsBetterFill(pairResult, best))
best = pairResult;
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 = RotationAnalysis.FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles);
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
if (groupParts.Count == 1)
{
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
var rectResult = FillRectangleBestFit(nestItem, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best))
best = rectResult;
var pairResult = FillWithPairs(nestItem, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}");
if (IsBetterFill(pairResult, best))
best = pairResult;
}
if (best == null || best.Count == 0)
return false;
Plate.Parts.AddRange(best);
return true;
}
public bool Pack(List<NestItem> items)
{
var workArea = Plate.WorkArea();
return PackArea(workArea, items);
}
public bool PackArea(Box box, List<NestItem> items)
{
var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
var engine = new PackBottomLeft(bin);
engine.Pack(binItems);
var parts = BinConverter.ToParts(bin, items);
Plate.Parts.AddRange(parts);
return parts.Count > 0;
}
private List<Part> FillRectangleBestFit(NestItem item, Box workArea)
{
var binItem = BinConverter.ToItem(item, Plate.PartSpacing);
var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing);
var engine = new FillBestFit(bin);
engine.Fill(binItem);
return BinConverter.ToParts(bin, new List<NestItem> { item });
}
private List<Part> FillWithPairs(NestItem item, Box workArea)
{
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, Plate.Size.Width, Plate.Size.Height,
Plate.PartSpacing);
var keptResults = bestFits.Where(r => r.Keep).Take(50).ToList();
Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {keptResults.Count}");
var resultBag = new System.Collections.Concurrent.ConcurrentBag<(int count, List<Part> parts)>();
System.Threading.Tasks.Parallel.For(0, keptResults.Count, i =>
{
var result = keptResults[i];
var pairParts = result.BuildParts(item.Drawing);
var angles = RotationAnalysis.FindHullEdgeAngles(pairParts);
var engine = new FillLinear(workArea, Plate.PartSpacing);
var filled = FillPattern(engine, pairParts, angles);
if (filled != null && filled.Count > 0)
resultBag.Add((filled.Count, filled));
});
List<Part> best = null;
foreach (var (count, parts) in resultBag)
{
if (best == null || count > best.Count)
best = parts;
}
Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts");
return best ?? new List<Part>();
}
private bool HasOverlaps(List<Part> parts, double spacing)
{
if (parts == null || parts.Count <= 1)
return false;
for (var i = 0; i < parts.Count; i++)
{
for (var j = i + 1; j < parts.Count; j++)
{
List<Vector> pts;
if (parts[i].Intersects(parts[j], out pts))
return true;
}
}
return false;
}
private bool IsBetterFill(List<Part> candidate, List<Part> current)
{
if (candidate == null || candidate.Count == 0)
return false;
if (current == null || current.Count == 0)
return true;
if (candidate.Count != current.Count)
return candidate.Count > current.Count;
// Same count: prefer smaller bounding box (more compact).
var candidateBox = ((IEnumerable<IBoundable>)candidate).GetBoundingBox();
var currentBox = ((IEnumerable<IBoundable>)current).GetBoundingBox();
return candidateBox.Area() < currentBox.Area();
}
private bool IsBetterValidFill(List<Part> candidate, List<Part> current)
{
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
return false;
return IsBetterFill(candidate, current);
}
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 (IsBetterValidFill(h, best))
best = h;
if (IsBetterValidFill(v, best))
best = v;
}
return best;
}
}
}