diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 5471123..b21e289 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -61,92 +61,91 @@ namespace OpenNest.Engine.Fill : NestDirection.Horizontal; } - /// - /// Computes the slide distance for the push algorithm, returning the - /// geometry-aware copy distance along the given axis. - /// - 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); - } - /// /// 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. /// - 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; } /// /// 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. /// - 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[parts.Count]; + var movingEntities = new List[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); + } - /// - /// Tests every pair of parts across adjacent pattern copies and returns the - /// maximum copy distance found. Returns 0 if no valid slide was found. - /// - private static double FindMaxPairDistance( - List 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; } - /// - /// Fast path for single-part patterns — no cross-part conflicts possible. - /// - private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary) - { - var template = patternA.Parts[0]; - return FindCopyDistance(template, direction, boundary); - } - - /// - /// Gets offset boundary lines for all parts in a pattern using a shared boundary. - /// - private static List GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction) - { - var lines = new List(); - - foreach (var part in pattern.Parts) - lines.AddRange(boundary.GetLines(part.Location, direction)); - - return lines; - } - - /// - /// Gets boundary lines for all parts in a pattern, with an additional - /// location offset applied. Avoids cloning the pattern. - /// - private static List GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction) - { - var lines = new List(); - - foreach (var part in pattern.Parts) - lines.AddRange(boundary.GetLines(part.Location + offset, direction)); - - return lines; - } - - /// - /// Creates boundaries for all parts in a pattern. Parts that share the same - /// program geometry (same drawing and rotation) reuse the same boundary instance. - /// - 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; - } - /// /// 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. /// - private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) + private List TilePattern(Pattern basePattern, NestDirection direction) { - var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); + var copyDistance = FindPatternCopyDistance(basePattern, direction); if (copyDistance <= 0) return new List(); @@ -394,11 +322,10 @@ namespace OpenNest.Engine.Fill private List FillGrid(Pattern pattern, NestDirection direction) { var perpAxis = PerpendicularAxis(direction); - var boundaries = CreateBoundaries(pattern); // Step 1: Tile along primary axis var row = new List(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(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;