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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user