Files
OpenNest/OpenNest.Engine/BestFit/BestFitFilter.cs
T
ajandClaude Opus 5.5 3d728b000e fix(bestfit): never hold a pair to a tighter aspect than one part
The best-fit filter rejected pairs over a 5:1 aspect (or the plate's)
unless 75% utilized. A long part such as U251-DET3 (27.4 x 3.5, aspect
7.7) is past that alone, so its tightest interlocked pairs (5.1:1) were
all dropped even though they are less elongated than the part itself.
On the 60 x 120 sheet that cost 12 parts: 66 placed instead of 78.

The limit now widens to the part's own bounding-box aspect, so
side-by-side pairs survive while end-to-end pairs of compact parts are
still rejected.

The learned-angle characterization nests re-captured: their 46 x 3 bar
now nests as an interlocked pair, which left the Fill nest identical to
a full sweep, so the notched quantity drops from 6 to 4 to keep the
fixture sensitive to learned angles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 07:58:31 -05:00

84 lines
3.1 KiB
C#

using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
public class BestFitFilter
{
public double MaxPlateWidth { get; set; }
public double MaxPlateHeight { get; set; }
public double MaxAspectRatio { get; set; } = 5.0;
/// <summary>
/// A high-aspect pair is kept anyway when this much of its rotated bounding box is
/// actual part area. Utilization is only ever an exception here, never a rejection:
/// thin-framed, hollow, or concave (e.g. S-shaped) parts have inherently low
/// part-to-bbox utilization, yet can nest tightly — their quality is judged by the
/// pair bounding-box area the results are sorted on, not by utilization.
/// </summary>
public double UtilizationOverride { get; set; } = 0.75;
public void Apply(List<BestFitResult> results)
{
var partAspects = new Dictionary<Drawing, double>(ReferenceEqualityComparer.Instance);
foreach (var result in results)
{
if (!result.Keep)
continue;
if (
result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight)
|| result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight)
)
{
result.Keep = false;
result.Reason = "Exceeds plate dimensions";
continue;
}
var aspect = result.LongestSide / result.ShortestSide;
var maxAspect = System.Math.Max(
MaxAspectRatio,
PartAspect(result.Candidate?.Drawing, partAspects)
);
if (aspect > maxAspect && result.Utilization < UtilizationOverride)
{
result.Keep = false;
result.Reason = string.Format(
"Aspect ratio {0:F1} exceeds max {1:0.#}",
aspect,
maxAspect
);
continue;
}
result.Reason = "Valid";
}
}
/// <summary>
/// A pair is never held to a tighter aspect limit than one part alone: two long, thin
/// parts laid side by side are less elongated than either, so a fixed limit would reject
/// every pair of a part longer than it. Measured on the drawing's own frame (canonical
/// for cached pairs); any other frame only understates the aspect.
/// </summary>
private static double PartAspect(Drawing drawing, Dictionary<Drawing, double> cache)
{
if (drawing?.Program == null)
return 0;
if (!cache.TryGetValue(drawing, out var aspect))
{
var box = drawing.Program.BoundingBox();
var shortest = System.Math.Min(box.Width, box.Length);
aspect =
shortest > 0 ? System.Math.Max(box.Width, box.Length) / shortest : 0;
cache[drawing] = aspect;
}
return aspect;
}
}
}