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.
This commit is contained in:
aj committed 2026-10-01 18:44:51 -04:00
1 parent c7b4ba4457
commit 25755a8850
1 file changed
+16 -8
@@ -25,7 +25,11 @@ internal sealed class Orientation
/// <summary>Chord deviation used for arcs; footprints are grown by it to stay conservative.</summary> /// <summary>Chord deviation used for arcs; footprints are grown by it to stay conservative.</summary>
public required double Tolerance { get; init; } public required double Tolerance { get; init; }
/// <summary>Outline bounds grown by the tolerance, so they contain the true perimeter.</summary> /// <summary>
/// Outline bounds grown by the outline's actual flattening error, so they contain the true
/// perimeter: by <see cref="Tolerance"/> when arcs were flattened, not at all for line-only
/// outlines, whose vertices are exact. Footprints and NFPs still use <see cref="Tolerance"/>.
/// </summary>
public required double MinX { get; init; } public required double MinX { get; init; }
public required double MinY { get; init; } public required double MinY { get; init; }
public required double MaxX { get; init; } public required double MaxX { get; init; }
@@ -96,7 +100,7 @@ internal static class PartCatalog
var outline = Polygonize(perimeter, angle, tolerance); var outline = Polygonize(perimeter, angle, tolerance);
if (outline.Count < 3) if (outline.Count < 3)
continue; 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)); var area = orientations.Count == 0 ? 0 : System.Math.Abs(Clipper.Area(orientations[0].Outline));
@@ -123,7 +127,7 @@ internal static class PartCatalog
return null; return null;
var tolerance = type.Orientations[0].Tolerance; var tolerance = type.Orientations[0].Tolerance;
var outline = Polygonize(type.Perimeter, angle, 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) => private static Shape? ReadPerimeter(PartGeometrySnapshot geometry) =>
@@ -163,9 +167,13 @@ internal static class PartCatalog
return path; 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); 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 return new Orientation
{ {
TypeIndex = typeIndex, TypeIndex = typeIndex,
@@ -173,10 +181,10 @@ internal static class PartCatalog
Rotation = angle, Rotation = angle,
Outline = outline, Outline = outline,
Tolerance = tolerance, Tolerance = tolerance,
MinX = bounds.left - tolerance, MinX = bounds.left - padding,
MinY = bounds.top - tolerance, // Clipper RectD: top is the minimum Y. MinY = bounds.top - padding, // Clipper RectD: top is the minimum Y.
MaxX = bounds.right + tolerance, MaxX = bounds.right + padding,
MaxY = bounds.bottom + tolerance, MaxY = bounds.bottom + padding,
}; };
} }