Files
OpenNest/OpenNest.Engine/Fill/GridDedup.cs
T
ajandClaude Opus 5.5 bcbcf1e611 perf(fill): cut a cold Lattice fill from ~300 s to ~160 s with identical layouts
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>
2026-11-07 19:34:31 -05:00

91 lines
2.8 KiB
C#

using System;
using System.Collections.Concurrent;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill;
/// <summary>
/// Tracks evaluated grid configurations so duplicate pattern/direction/workArea
/// combinations can be skipped across fill strategies.
/// </summary>
public class GridDedup
{
public const string SharedStateKey = "GridDedup";
private readonly ConcurrentDictionary<GridKey, byte> _seen = new();
/// <summary>
/// Returns true if this configuration has NOT been seen before (i.e., should be evaluated).
/// Returns false if it's a duplicate.
/// </summary>
public bool TryAdd(Box patternBox, Box workArea, NestDirection dir)
{
var key = new GridKey(patternBox, workArea, dir);
return _seen.TryAdd(key, 0);
}
/// <summary>
/// True if <see cref="TryAdd"/> already accepted this configuration. Claims are never
/// removed, so a configuration seen now stays rejected.
/// </summary>
public bool Contains(Box patternBox, Box workArea, NestDirection dir) =>
_seen.ContainsKey(new GridKey(patternBox, workArea, dir));
public int Count => _seen.Count;
/// <summary>
/// Gets or creates a GridDedup from FillContext.SharedState.
/// </summary>
public static GridDedup GetOrCreate(
System.Collections.Generic.Dictionary<string, object> sharedState
)
{
if (sharedState.TryGetValue(SharedStateKey, out var existing))
return (GridDedup)existing;
var dedup = new GridDedup();
sharedState[SharedStateKey] = dedup;
return dedup;
}
private readonly struct GridKey : IEquatable<GridKey>
{
private readonly int _patternW,
_patternL,
_workW,
_workL,
_dir;
public GridKey(Box patternBox, Box workArea, NestDirection dir)
{
_patternW = (int)System.Math.Round(patternBox.Width * 10);
_patternL = (int)System.Math.Round(patternBox.Length * 10);
_workW = (int)System.Math.Round(workArea.Width * 10);
_workL = (int)System.Math.Round(workArea.Length * 10);
_dir = (int)dir;
}
public bool Equals(GridKey other) =>
_patternW == other._patternW
&& _patternL == other._patternL
&& _workW == other._workW
&& _workL == other._workL
&& _dir == other._dir;
public override bool Equals(object obj) => obj is GridKey other && Equals(other);
public override int GetHashCode()
{
unchecked
{
var hash = _patternW;
hash = hash * 397 ^ _patternL;
hash = hash * 397 ^ _workW;
hash = hash * 397 ^ _workL;
hash = hash * 397 ^ _dir;
return hash;
}
}
}
}