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>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-07 19:34:31 -05:00
1 parent 331234d6f3
commit bcbcf1e611
10 files changed
+816 -109

No files matched your search

+29 -3
View File
@@ -743,16 +743,40 @@ namespace OpenNest.Geometry
Vector direction,
SlideContactClassifier contacts
)
{
return DirectionalDistance(
movingEntities,
ExtractEntityVertices(movingEntities),
stationaryEntities,
ExtractEntityVertices(stationaryEntities),
direction,
contacts
);
}
/// <summary>
/// <see cref="DirectionalDistance(List{Entity}, List{Entity}, Vector, SlideContactClassifier)"/>
/// with each list's <see cref="ExtractEntityVertices"/> result supplied by a caller that
/// slides one list against several others. The vertices must come from these exact lists.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities,
Vector[] movingVertices,
List<Entity> stationaryEntities,
Vector[] stationaryVertices,
Vector direction,
SlideContactClassifier contacts
)
{
// Phases: vertex rays both ways, arc-to-line closest points (vertex sampling
// misses interior arc contact), then native curve tangency.
var source = new EntitySlideEvents(
movingEntities,
ExtractEntityVertices(movingEntities),
movingVertices,
0,
0,
stationaryEntities,
ExtractEntityVertices(stationaryEntities),
stationaryVertices,
direction.X,
direction.Y,
arcToLine: true
@@ -762,7 +786,9 @@ namespace OpenNest.Geometry
public static Vector[] ExtractEntityVertices(List<Entity> entities)
{
var vertices = new HashSet<Vector>();
// Presized: a set without removals enumerates in insertion order whatever its
// capacity, so this only saves the rehashes of growing from empty.
var vertices = new HashSet<Vector>(entities.Count * 2);
for (var i = 0; i < entities.Count; i++)
{