From 25755a88507b504342cec5499014c7ec2574c9f0 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 1 Oct 2026 18:44:51 -0400 Subject: [PATCH] fix(irregular): keep exact bounds for line-only part outlines Orientation bounds were padded by the arc chord tolerance even when the outline has no arcs, so a part that exactly fills the work area after rotation was rejected before packing. Pad only outlines with flattened arcs; footprints and NFPs still use the chord tolerance. --- .../NestingEngines/Irregular/PartCatalog.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/OpenNest.Engine/NestingEngines/Irregular/PartCatalog.cs b/OpenNest.Engine/NestingEngines/Irregular/PartCatalog.cs index 0e42ceb..445150e 100644 --- a/OpenNest.Engine/NestingEngines/Irregular/PartCatalog.cs +++ b/OpenNest.Engine/NestingEngines/Irregular/PartCatalog.cs @@ -25,7 +25,11 @@ internal sealed class Orientation /// Chord deviation used for arcs; footprints are grown by it to stay conservative. public required double Tolerance { get; init; } - /// Outline bounds grown by the tolerance, so they contain the true perimeter. + /// + /// Outline bounds grown by the outline's actual flattening error, so they contain the true + /// perimeter: by when arcs were flattened, not at all for line-only + /// outlines, whose vertices are exact. Footprints and NFPs still use . + /// public required double MinX { get; init; } public required double MinY { get; init; } public required double MaxX { get; init; } @@ -96,7 +100,7 @@ internal static class PartCatalog var outline = Polygonize(perimeter, angle, tolerance); if (outline.Count < 3) continue; - orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance)); + orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance, perimeter)); } var area = orientations.Count == 0 ? 0 : System.Math.Abs(Clipper.Area(orientations[0].Outline)); @@ -123,7 +127,7 @@ internal static class PartCatalog return null; var tolerance = type.Orientations[0].Tolerance; var outline = Polygonize(type.Perimeter, angle, tolerance); - return outline.Count < 3 ? null : MakeOrientation(type.Index, index, angle, outline, tolerance); + return outline.Count < 3 ? null : MakeOrientation(type.Index, index, angle, outline, tolerance, type.Perimeter); } private static Shape? ReadPerimeter(PartGeometrySnapshot geometry) => @@ -163,9 +167,13 @@ internal static class PartCatalog return path; } - private static Orientation MakeOrientation(int typeIndex, int index, double angle, PathD outline, double tolerance) + private static Orientation MakeOrientation(int typeIndex, int index, double angle, PathD outline, + double tolerance, Shape perimeter) { var bounds = Clipper.GetBounds(outline); + // Only flattened arcs deviate from the true perimeter; line vertices are exact, so a + // line-only part keeps its nominal material bounds and can fill its work area exactly. + var padding = perimeter.Entities.All(e => e.Type == EntityType.Line) ? 0 : tolerance; return new Orientation { TypeIndex = typeIndex, @@ -173,10 +181,10 @@ internal static class PartCatalog Rotation = angle, Outline = outline, Tolerance = tolerance, - MinX = bounds.left - tolerance, - MinY = bounds.top - tolerance, // Clipper RectD: top is the minimum Y. - MaxX = bounds.right + tolerance, - MaxY = bounds.bottom + tolerance, + MinX = bounds.left - padding, + MinY = bounds.top - padding, // Clipper RectD: top is the minimum Y. + MaxX = bounds.right + padding, + MaxY = bounds.bottom + padding, }; }