perf(fill): reuse offset geometry for translated copies

FillLinear re-prepared offset perimeter geometry (ConvertProgram ->
ShapeProfile -> OffsetOutward) for every part it measured, although
tiled copies share one Program and differ only by Location. A CPU
profile of a 169-part Default job put 62% of wall time there.

Prepare each distinct Program (reference identity) once per public
Fill/FillRow call in local frame, then clone and translate for each
location. The cache is created per call and passed down privately
because FillHelpers.FillPattern calls Fill concurrently on one
instance. PartGeometry gains a local-frame Program overload that the
Part overload now delegates to.

Evaluation order, lazy preparation, fallbacks and tiling are
unchanged. Differential tests against a frozen copy of the previous
FillLinear check bitwise equality, including concurrent calls; Debug
work tests pin preparation counts. With the thread pool capped at one
worker, before/after whole-job layouts are byte-identical. The
Default corpus job median drops from 40,715 to 18,810 ms.
This commit is contained in:
aj
2026-09-26 20:24:34 -04:00
parent 094c4c196b
commit 0df2587cf2
8 changed files with 1155 additions and 36 deletions
+48 -26
View File
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -8,6 +7,35 @@ namespace OpenNest.Engine.Fill
{
public class FillLinear
{
// Owned by one public call: FillHelpers can use this filler concurrently.
// Cached local entities never escape; only translated clones reach spatial queries.
private sealed class OffsetPerimeterCache
{
private readonly Dictionary<CNC.Program, List<Entity>> perimeters =
new Dictionary<CNC.Program, List<Entity>>(ReferenceEqualityComparer.Instance);
private readonly double spacing;
public OffsetPerimeterCache(double spacing) => this.spacing = spacing;
public List<Entity> AtLocation(CNC.Program program, Vector location)
{
if (!perimeters.TryGetValue(program, out var local))
{
local = PartGeometry.GetOffsetPerimeterEntities(program, spacing);
perimeters.Add(program, local);
}
var result = new List<Entity>(local.Count);
foreach (var entity in local)
{
var clone = entity.Clone();
clone.Offset(location);
result.Add(clone);
}
return result;
}
}
public FillLinear(Box workArea, double partSpacing)
{
PartSpacing = partSpacing;
@@ -64,18 +92,15 @@ namespace OpenNest.Engine.Fill
/// Uses native Line/Arc entities (inflated by half-spacing) so curves are handled
/// exactly without polygon sampling error.
/// </summary>
private double FindCopyDistance(Part partA, NestDirection direction)
private double FindCopyDistance(Part partA, NestDirection direction, OffsetPerimeterCache cache)
{
var bboxDim = GetDimension(partA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
var offset = MakeOffset(direction, startOffset);
var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
partA.CloneAtOffset(offset),
HalfSpacing
);
var stationaryEntities = cache.AtLocation(partA.Program, partA.Location);
var movingEntities = cache.AtLocation(partA.Program, partA.Location + offset);
var slideDistance = SpatialQuery.DirectionalDistance(
movingEntities,
@@ -96,10 +121,10 @@ namespace OpenNest.Engine.Fill
/// geometry inflated by half-spacing — same primitive the Compactor uses — so arcs
/// are exact and no bbox clamp is needed.
/// </summary>
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction)
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, OffsetPerimeterCache cache)
{
if (patternA.Parts.Count == 1)
return FindCopyDistance(patternA.Parts[0], direction);
return FindCopyDistance(patternA.Parts[0], direction, cache);
var bboxDim = GetDimension(patternA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
@@ -142,14 +167,8 @@ namespace OpenNest.Engine.Fill
if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
continue;
stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
parts[i],
HalfSpacing
);
movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
parts[j].CloneAtOffset(offset),
HalfSpacing
);
stationaryEntities[i] ??= cache.AtLocation(parts[i].Program, parts[i].Location);
movingEntities[j] ??= cache.AtLocation(parts[j].Program, parts[j].Location + offset);
var slideDistance = SpatialQuery.DirectionalDistance(
movingEntities[j],
@@ -176,9 +195,9 @@ namespace OpenNest.Engine.Fill
/// patterns, also adds individual parts from the next incomplete copy
/// that still fit within the work area.
/// </summary>
private List<Part> TilePattern(Pattern basePattern, NestDirection direction)
private List<Part> TilePattern(Pattern basePattern, NestDirection direction, OffsetPerimeterCache cache)
{
var copyDistance = FindPatternCopyDistance(basePattern, direction);
var copyDistance = FindPatternCopyDistance(basePattern, direction, cache);
if (copyDistance <= 0)
return new List<Part>();
@@ -337,13 +356,13 @@ namespace OpenNest.Engine.Fill
/// a row, then tiling that row along the perpendicular axis to form a grid.
/// After the grid is formed, fills the remaining strip with individual parts.
/// </summary>
private List<Part> FillGrid(Pattern pattern, NestDirection direction)
private List<Part> FillGrid(Pattern pattern, NestDirection direction, OffsetPerimeterCache cache)
{
var perpAxis = PerpendicularAxis(direction);
// Step 1: Tile along primary axis
var row = new List<Part>(pattern.Parts);
row.AddRange(TilePattern(pattern, direction));
row.AddRange(TilePattern(pattern, direction, cache));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
{
@@ -355,7 +374,7 @@ namespace OpenNest.Engine.Fill
// If primary tiling didn't produce copies, just tile along perpendicular
if (row.Count <= pattern.Parts.Count)
{
row.AddRange(TilePattern(pattern, perpAxis));
row.AddRange(TilePattern(pattern, perpAxis, cache));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
{
@@ -373,7 +392,7 @@ namespace OpenNest.Engine.Fill
rowPattern.UpdateBounds();
var gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePattern(rowPattern, perpAxis));
gridResult.AddRange(TilePattern(rowPattern, perpAxis, cache));
if (HasOverlappingParts(gridResult, out var a3, out var b3))
{
@@ -435,6 +454,7 @@ namespace OpenNest.Engine.Fill
/// </summary>
public Pattern FillRow(Drawing drawing, double rotationAngle, NestDirection direction)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
@@ -442,7 +462,7 @@ namespace OpenNest.Engine.Fill
var template = seed.Parts[0];
var copyDistance = FindCopyDistance(template, direction);
var copyDistance = FindCopyDistance(template, direction, cache);
if (copyDistance <= 0)
return seed;
@@ -474,6 +494,7 @@ namespace OpenNest.Engine.Fill
/// </summary>
public List<Part> Fill(Pattern pattern, NestDirection primaryAxis)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
if (pattern.Parts.Count == 0)
return new List<Part>();
@@ -486,7 +507,7 @@ namespace OpenNest.Engine.Fill
)
return new List<Part>();
return FillGrid(basePattern, primaryAxis);
return FillGrid(basePattern, primaryAxis, cache);
}
/// <summary>
@@ -495,12 +516,13 @@ namespace OpenNest.Engine.Fill
/// </summary>
public List<Part> Fill(Drawing drawing, double rotationAngle, NestDirection primaryAxis)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
return new List<Part>();
return FillGrid(seed, primaryAxis);
return FillGrid(seed, primaryAxis, cache);
}
}
}