refactor(fill): use native entity geometry for linear copy distance
Replaces PartBoundary polygon edges with PartGeometry.GetOffsetPerimeterEntities (inflated Line/Arc entities) so arcs are handled exactly without the polygon sampling error that previously required a bboxDim + PartSpacing clamp. Adds bbox DirectionalGap / PerpendicularOverlap early-outs to skip pair checks that can't produce a valid slide, and removes the now-unused PartBoundary cache, GetPatternLines/GetOffsetPatternLines helpers, and ComputeCopyDistance clamp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,92 +61,91 @@ namespace OpenNest.Engine.Fill
|
||||
: NestDirection.Horizontal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the slide distance for the push algorithm, returning the
|
||||
/// geometry-aware copy distance along the given axis.
|
||||
/// </summary>
|
||||
private double ComputeCopyDistance(double bboxDim, double slideDistance)
|
||||
{
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
return bboxDim + PartSpacing;
|
||||
|
||||
// The geometry-aware slide can produce a copy distance smaller than
|
||||
// the part itself when inflated corner/arc vertices interact spuriously.
|
||||
// Clamp to bboxDim + PartSpacing to prevent bounding box overlap.
|
||||
return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the geometry-aware copy distance between two identical parts along an axis.
|
||||
/// Both parts are inflated by half-spacing for symmetric spacing.
|
||||
/// 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, PartBoundary boundary)
|
||||
private double FindCopyDistance(Part partA, NestDirection direction)
|
||||
{
|
||||
var bboxDim = GetDimension(partA.BoundingBox, direction);
|
||||
var pushDir = GetPushDirection(direction);
|
||||
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
||||
var offset = MakeOffset(direction, startOffset);
|
||||
|
||||
var locationBOffset = MakeOffset(direction, bboxDim);
|
||||
var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
|
||||
var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
|
||||
partA.CloneAtOffset(offset), HalfSpacing);
|
||||
|
||||
// Use the most efficient array-based overload to avoid all allocations.
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
boundary.GetEdges(pushDir), partA.Location + locationBOffset,
|
||||
boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location,
|
||||
pushDir);
|
||||
movingEntities, stationaryEntities, pushDir);
|
||||
|
||||
return ComputeCopyDistance(bboxDim, slideDistance);
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
return bboxDim + PartSpacing;
|
||||
|
||||
return startOffset - slideDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
|
||||
/// Checks every pair of parts across adjacent patterns so that multi-part
|
||||
/// patterns (e.g. interlocking pairs) maintain spacing between ALL parts.
|
||||
/// Both sides are inflated by half-spacing for symmetric spacing.
|
||||
/// Checks every pair of parts across adjacent pattern copies so multi-part patterns
|
||||
/// (e.g. interlocking pairs) maintain spacing between ALL parts. Uses native entity
|
||||
/// 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, PartBoundary[] boundaries)
|
||||
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction)
|
||||
{
|
||||
if (patternA.Parts.Count <= 1)
|
||||
return FindSinglePartPatternCopyDistance(patternA, direction, boundaries[0]);
|
||||
if (patternA.Parts.Count == 1)
|
||||
return FindCopyDistance(patternA.Parts[0], direction);
|
||||
|
||||
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
||||
var pushDir = GetPushDirection(direction);
|
||||
var opposite = SpatialQuery.OppositeDirection(pushDir);
|
||||
var dirVec = SpatialQuery.DirectionToOffset(pushDir, 1.0);
|
||||
|
||||
// bboxDim already spans max(upper) - min(lower) across all parts,
|
||||
// so the start offset just needs to push beyond that plus spacing.
|
||||
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
||||
var offset = MakeOffset(direction, startOffset);
|
||||
|
||||
var maxCopyDistance = FindMaxPairDistance(
|
||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
||||
var parts = patternA.Parts;
|
||||
var stationaryBoxes = new Box[parts.Count];
|
||||
var movingBoxes = new Box[parts.Count];
|
||||
var stationaryEntities = new List<Entity>[parts.Count];
|
||||
var movingEntities = new List<Entity>[parts.Count];
|
||||
|
||||
// The copy distance must be at least bboxDim + PartSpacing to prevent
|
||||
// bounding box overlap. Cross-pair slides can underestimate when the
|
||||
// circumscribed polygon boundary overshoots the true arc, creating
|
||||
// spurious contacts between diagonal parts in adjacent copies.
|
||||
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
|
||||
}
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
stationaryBoxes[i] = parts[i].BoundingBox;
|
||||
movingBoxes[i] = stationaryBoxes[i].Translate(offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests every pair of parts across adjacent pattern copies and returns the
|
||||
/// maximum copy distance found. Returns 0 if no valid slide was found.
|
||||
/// </summary>
|
||||
private static double FindMaxPairDistance(
|
||||
List<Part> parts, PartBoundary[] boundaries, Vector offset,
|
||||
PushDirection pushDir, PushDirection opposite, double startOffset)
|
||||
{
|
||||
var maxCopyDistance = 0.0;
|
||||
|
||||
for (var j = 0; j < parts.Count; j++)
|
||||
{
|
||||
var movingEdges = boundaries[j].GetEdges(pushDir);
|
||||
var locationB = parts[j].Location + offset;
|
||||
var movingBox = movingBoxes[j];
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var stationaryBox = stationaryBoxes[i];
|
||||
|
||||
// Skip if stationary is already ahead of moving in the push direction
|
||||
// (sliding forward would take them further apart).
|
||||
if (SpatialQuery.DirectionalGap(movingBox, stationaryBox, opposite) > 0)
|
||||
continue;
|
||||
|
||||
// Skip if bboxes can't overlap along the axis perpendicular to the push.
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
|
||||
continue;
|
||||
|
||||
stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[i], HalfSpacing);
|
||||
movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[j].CloneAtOffset(offset), HalfSpacing);
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEdges, locationB,
|
||||
boundaries[i].GetEdges(opposite), parts[i].Location,
|
||||
pushDir);
|
||||
movingEntities[j], stationaryEntities[i], pushDir);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
continue;
|
||||
@@ -161,86 +160,15 @@ namespace OpenNest.Engine.Fill
|
||||
return maxCopyDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fast path for single-part patterns — no cross-part conflicts possible.
|
||||
/// </summary>
|
||||
private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
|
||||
{
|
||||
var template = patternA.Parts[0];
|
||||
return FindCopyDistance(template, direction, boundary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets offset boundary lines for all parts in a pattern using a shared boundary.
|
||||
/// </summary>
|
||||
private static List<Line> GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction)
|
||||
{
|
||||
var lines = new List<Line>();
|
||||
|
||||
foreach (var part in pattern.Parts)
|
||||
lines.AddRange(boundary.GetLines(part.Location, direction));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets boundary lines for all parts in a pattern, with an additional
|
||||
/// location offset applied. Avoids cloning the pattern.
|
||||
/// </summary>
|
||||
private static List<Line> GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction)
|
||||
{
|
||||
var lines = new List<Line>();
|
||||
|
||||
foreach (var part in pattern.Parts)
|
||||
lines.AddRange(boundary.GetLines(part.Location + offset, direction));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates boundaries for all parts in a pattern. Parts that share the same
|
||||
/// program geometry (same drawing and rotation) reuse the same boundary instance.
|
||||
/// </summary>
|
||||
private PartBoundary[] CreateBoundaries(Pattern pattern)
|
||||
{
|
||||
var boundaries = new PartBoundary[pattern.Parts.Count];
|
||||
var cache = new List<(Drawing drawing, double rotation, PartBoundary boundary)>();
|
||||
|
||||
for (var i = 0; i < pattern.Parts.Count; i++)
|
||||
{
|
||||
var part = pattern.Parts[i];
|
||||
PartBoundary found = null;
|
||||
|
||||
foreach (var entry in cache)
|
||||
{
|
||||
if (entry.drawing == part.BaseDrawing && entry.rotation.IsEqualTo(part.Rotation))
|
||||
{
|
||||
found = entry.boundary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == null)
|
||||
{
|
||||
found = new PartBoundary(part, HalfSpacing);
|
||||
cache.Add((part.BaseDrawing, part.Rotation, found));
|
||||
}
|
||||
|
||||
boundaries[i] = found;
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tiles a pattern along the given axis, returning the cloned parts
|
||||
/// (does not include the original pattern's parts). For multi-part
|
||||
/// 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, PartBoundary[] boundaries)
|
||||
private List<Part> TilePattern(Pattern basePattern, NestDirection direction)
|
||||
{
|
||||
var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
|
||||
var copyDistance = FindPatternCopyDistance(basePattern, direction);
|
||||
|
||||
if (copyDistance <= 0)
|
||||
return new List<Part>();
|
||||
@@ -394,11 +322,10 @@ namespace OpenNest.Engine.Fill
|
||||
private List<Part> FillGrid(Pattern pattern, NestDirection direction)
|
||||
{
|
||||
var perpAxis = PerpendicularAxis(direction);
|
||||
var boundaries = CreateBoundaries(pattern);
|
||||
|
||||
// Step 1: Tile along primary axis
|
||||
var row = new List<Part>(pattern.Parts);
|
||||
row.AddRange(TilePattern(pattern, direction, boundaries));
|
||||
row.AddRange(TilePattern(pattern, direction));
|
||||
|
||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
|
||||
{
|
||||
@@ -410,7 +337,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, boundaries));
|
||||
row.AddRange(TilePattern(pattern, perpAxis));
|
||||
|
||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
|
||||
{
|
||||
@@ -427,9 +354,8 @@ namespace OpenNest.Engine.Fill
|
||||
rowPattern.Parts.AddRange(row);
|
||||
rowPattern.UpdateBounds();
|
||||
|
||||
var rowBoundaries = CreateBoundaries(rowPattern);
|
||||
var gridResult = new List<Part>(rowPattern.Parts);
|
||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
|
||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis));
|
||||
|
||||
if (HasOverlappingParts(gridResult, out var a3, out var b3))
|
||||
{
|
||||
@@ -481,9 +407,8 @@ namespace OpenNest.Engine.Fill
|
||||
return seed;
|
||||
|
||||
var template = seed.Parts[0];
|
||||
var boundary = new PartBoundary(template, HalfSpacing);
|
||||
|
||||
var copyDistance = FindCopyDistance(template, direction, boundary);
|
||||
var copyDistance = FindCopyDistance(template, direction);
|
||||
|
||||
if (copyDistance <= 0)
|
||||
return seed;
|
||||
|
||||
Reference in New Issue
Block a user