Files
OpenNest/OpenNest.Engine/Strategies/FillHelpers.cs
AJ Isaacs ef15421915 refactor: standardize fill strategy progress reporting via FillContext
Strategies and fillers previously called NestEngineBase.ReportProgress
directly, each constructing ProgressReport structs with phase, plate
number, and work area manually. Some strategies (RectBestFit) reported
nothing at all. This made progress updates inconsistent and flakey.

Add FillContext.ReportProgress(parts, description) as the single
standard method for intermediate progress. RunPipeline sets ActivePhase
before each strategy, and the context handles common fields. Lower-level
fillers (PairFiller, FillExtents, StripeFiller) now accept an
Action<List<Part>, string> callback instead of raw IProgress, removing
their coupling to NestEngineBase and ProgressReport.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:21:48 -04:00

180 lines
6.1 KiB
C#

using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace OpenNest.Engine.Strategies
{
public static class FillHelpers
{
public static 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;
}
public static List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea, IFillComparer comparer = null)
{
var results = new ConcurrentBag<(List<Part> Parts, FillScore Score)>();
Parallel.ForEach(angles, angle =>
{
var pattern = BuildRotatedPattern(groupParts, angle);
if (pattern.Parts.Count == 0)
return;
var h = engine.Fill(pattern, NestDirection.Horizontal);
if (h != null && h.Count > 0)
results.Add((h, FillScore.Compute(h, workArea)));
var v = engine.Fill(pattern, NestDirection.Vertical);
if (v != null && v.Count > 0)
results.Add((v, FillScore.Compute(v, workArea)));
});
List<Part> best = null;
var bestScore = default(FillScore);
foreach (var res in results)
{
if (comparer != null)
{
if (best == null || comparer.IsBetter(res.Parts, best, workArea))
best = res.Parts;
}
else
{
if (best == null || res.Score > bestScore)
{
best = res.Parts;
bestScore = res.Score;
}
}
}
return best;
}
/// <summary>
/// Runs a fill function with direction preference logic.
/// If preferred is null, tries both directions and returns the better result.
/// If preferred is set, tries preferred first; only tries other if preferred yields zero.
/// </summary>
public static List<Part> FillWithDirectionPreference(
Func<NestDirection, List<Part>> fillFunc,
NestDirection? preferred,
IFillComparer comparer,
Box workArea)
{
if (preferred == null)
{
var h = fillFunc(NestDirection.Horizontal);
var v = fillFunc(NestDirection.Vertical);
if ((h == null || h.Count == 0) && (v == null || v.Count == 0))
return new List<Part>();
if (h == null || h.Count == 0) return v;
if (v == null || v.Count == 0) return h;
return comparer.IsBetter(h, v, workArea) ? h : v;
}
var other = preferred == NestDirection.Horizontal
? NestDirection.Vertical
: NestDirection.Horizontal;
var pref = fillFunc(preferred.Value);
if (pref != null && pref.Count > 0)
return pref;
var fallback = fillFunc(other);
return fallback ?? new List<Part>();
}
/// <summary>
/// Sweeps a list of angles, calling fillAtAngle for each, and returns
/// the best result according to the context's comparer. Handles
/// cancellation and progress reporting via context.ReportProgress.
/// </summary>
public static List<Part> BestOverAngles(
FillContext context,
IReadOnlyList<double> angles,
Func<double, List<Part>> fillAtAngle,
string phaseLabel)
{
var workArea = context.WorkArea;
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
List<Part> best = null;
for (var i = 0; i < angles.Count; i++)
{
context.Token.ThrowIfCancellationRequested();
var angle = angles[i];
var result = fillAtAngle(angle);
var angleDeg = Angle.ToDegrees(angle);
if (result != null && result.Count > 0)
{
if (best == null || comparer.IsBetter(result, best, workArea))
best = result;
}
context.ReportProgress(best,
$"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts");
}
return best ?? new List<Part>();
}
/// <summary>
/// Checks if any pair of parts geometrically overlap. Uses bounding box
/// pre-filtering for performance, then falls back to shape intersection.
/// </summary>
internal static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
}
}