Files
OpenNest/OpenNest.Engine/Strategies/FillHelpers.cs
T
aj a27290a29c perf(fill): prepare overlap polygons once per check
Both HasOverlappingParts loops rebuilt each part's polygon from its
Program on every pair. PartOverlapChecker prepares each distinct Program
(reference identity) once and each part's world polygon once per call,
then uses the overlap-only Collision.HasOverlap. Loop order, bounding-box
prefilter, early exit and returned indices are unchanged; Part.Intersects
shares the material/polygon recipe and still returns crossing points.

Verification:
- Frozen LegacyPartOverlap differential (original Intersects and both
  loops): verdicts, indices and world polygons bit-identical across fill
  grids, patterns, touching/epsilon gaps, scribe/rapid/empty programs.
- Debug OverlapPolygonPreparations: 246 -> 1 and 64 -> 2 per check.
- Corpus job (169 parts, --engines Default --parallel 1, with 1a):
  median 18,885 -> 13,464 ms over 4+4 alternating runs, identical
  outcomes; serialized layout byte-identical to the base.
2026-09-27 13:34:04 -04:00

200 lines
6.6 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
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();
// Keep the recompute: at angle 0, accumulated source translations can
// leave a different cached box than Program.BoundingBox() + Location.
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, comparer == null ? FillScore.Compute(h, workArea) : default));
var v = engine.Fill(pattern, NestDirection.Vertical);
if (v != null && v.Count > 0)
results.Add((v, comparer == null ? FillScore.Compute(v, workArea) : default));
}
);
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)
{
var checker = new PartOverlapChecker();
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 (checker.Overlaps(parts[i], parts[j]))
return true;
}
}
return false;
}
}
}