mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 19:25:43 -05:00
The first fill of a new drawing was blamed on the best-fit pair search, but on the reported nest that search takes ~3.4 s. The time goes to FillLinear.FillGrid, run thousands of times by the Pairs, Row/Column and Linear strategies: pattern copy-distance slides and the grid overlap check. Every change keeps the output bit for bit (part poses compared on the benchmark nest; best-fit results unchanged). - ConvexDecomposition.Triangulate: unlink clipped vertices from a circular list instead of List.RemoveAt, and remember why a vertex failed the ear test (same neighbors, same blocking vertex still present) instead of retesting it after every ear. Compared bitwise against a frozen pre-change copy, including the blocker-clipped case. - Collision: skip triangles whose box misses the other triangulation's union box (exact: min/max and monotone subtraction), reuse each triangle's open vertices, and let the overlap-only path stop at its first region when there are no holes. Check still returns every region. - SpatialQuery.ExtractEntityVertices presizes its set; FillLinear extracts each entity list's vertices once per copy-distance search rather than once per pair. - StripeFiller converges its stripe angles and tiles its grids in parallel. Dedup claims, cached remnant fills, ranking and progress stay in candidate order, and grids are tiled only for configurations a claim can still win (GridDedup.Contains), so results and reports do not depend on the thread count. - LinearFillStrategy fills its angles in parallel through FillHelpers.BestOverAnglesInParallel; angle results, ranking and progress stay in order. Benchmark (one drawing, 60x120, 0.25 spacing, cleared plate, interleaved runs): FillItem Lattice 291/317 s -> 164/153 s, 132 parts both; PairFiller alone 48 s -> 38-42 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
8.6 KiB
C#
254 lines
8.6 KiB
C#
using System;
|
|
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
|
|
)
|
|
{
|
|
// Slots in angle order, V before H within an angle: ties keep the earliest slot
|
|
// however the workers finish.
|
|
var results = new (List<Part> Parts, FillScore Score)[angles.Count * 2];
|
|
|
|
Parallel.For(
|
|
0,
|
|
angles.Count,
|
|
i =>
|
|
{
|
|
var pattern = BuildRotatedPattern(groupParts, angles[i]);
|
|
|
|
if (pattern.Parts.Count == 0)
|
|
return;
|
|
|
|
var h = engine.Fill(pattern, NestDirection.Horizontal);
|
|
if (h != null && h.Count > 0)
|
|
results[2 * i + 1] = (h, comparer == null ? FillScore.Compute(h, workArea) : default);
|
|
|
|
var v = engine.Fill(pattern, NestDirection.Vertical);
|
|
if (v != null && v.Count > 0)
|
|
results[2 * i] = (v, comparer == null ? FillScore.Compute(v, workArea) : default);
|
|
}
|
|
);
|
|
|
|
List<Part> best = null;
|
|
var bestScore = default(FillScore);
|
|
|
|
foreach (var res in results)
|
|
{
|
|
if (res.Parts == null)
|
|
continue;
|
|
|
|
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>
|
|
/// <see cref="BestOverAngles"/> for a pure per-angle fill: every angle is filled in
|
|
/// parallel, then <paramref name="recordResult"/>, the ranking and the progress reports run
|
|
/// once per angle in angle order, so the result does not depend on the thread count.
|
|
/// <paramref name="fillAtAngle"/> must not touch shared state; side effects of an angle's
|
|
/// result belong in <paramref name="recordResult"/>.
|
|
/// </summary>
|
|
public static List<Part> BestOverAnglesInParallel(
|
|
FillContext context,
|
|
IReadOnlyList<double> angles,
|
|
Func<double, List<Part>> fillAtAngle,
|
|
Action<double, List<Part>> recordResult,
|
|
string phaseLabel
|
|
)
|
|
{
|
|
var results = new List<Part>[angles.Count];
|
|
|
|
try
|
|
{
|
|
Parallel.For(
|
|
0,
|
|
angles.Count,
|
|
new ParallelOptions { CancellationToken = context.Token },
|
|
i => results[i] = fillAtAngle(angles[i])
|
|
);
|
|
}
|
|
catch (AggregateException ex)
|
|
{
|
|
// Surface the failure itself, cancellation included, as the sequential sweep did.
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
|
.Capture(ex.InnerExceptions[0])
|
|
.Throw();
|
|
throw;
|
|
}
|
|
|
|
var next = 0;
|
|
return BestOverAngles(
|
|
context,
|
|
angles,
|
|
angle =>
|
|
{
|
|
var result = results[next++];
|
|
recordResult(angle, result);
|
|
return result;
|
|
},
|
|
phaseLabel
|
|
);
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|