From bfc36d166cd593cd76ad8285cf913f767d2918a1 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 2 Nov 2026 16:54:58 -0500 Subject: [PATCH] fix(support): measure a part the same however it is turned A 5 x 5 right triangle fits both its legs' square and its hypotenuse's 7.07 x 3.54 box, each of area 25, and rotating calipers kept whichever rounding found first, so the same gusset measured 5 long at one rotation and 7.07 at another. Of rectangles tied for least area, measure the one with the shorter long side, so length, width and the tab tie-break no longer depend on placement. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Core/Support/SupportAnalyzer.cs | 44 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/OpenNest.Core/Support/SupportAnalyzer.cs b/OpenNest.Core/Support/SupportAnalyzer.cs index f9b8b678..3071aac8 100644 --- a/OpenNest.Core/Support/SupportAnalyzer.cs +++ b/OpenNest.Core/Support/SupportAnalyzer.cs @@ -180,9 +180,47 @@ namespace OpenNest.Support return new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); } - // The full angle range checks every hull edge. - private static BoundingRectangleResult BoundingRectangle(IReadOnlyList outline) => - RotatingCalipers.MinimumBoundingRectangle(ConvexHull.Compute(outline.ToList()), 0, Math.Angle.TwoPI); + // The minimum-area rectangle over every hull edge. Shapes such as a right isosceles + // triangle have more than one (its legs' square and its hypotenuse's 2:1 box have the same + // area), and which one rounding picks depends on the part's rotation; of rectangles tied + // for area, the one with the shorter long side wins, so a drawing measures the same + // however it is placed. + private static BoundingRectangleResult BoundingRectangle(IReadOnlyList outline) + { + var hull = ConvexHull.Compute(outline.ToList()).Vertices; + var n = hull.Count > 1 && hull[0] == hull[^1] ? hull.Count - 1 : hull.Count; + if (n < 3) + return RotatingCalipers.MinimumBoundingRectangle(outline.ToList()); + + var candidates = new List(n); + for (var i = 0; i < n; i++) + { + var edge = hull[(i + 1) % n] - hull[i]; + var length = System.Math.Sqrt(edge.X * edge.X + edge.Y * edge.Y); + if (length <= Math.Tolerance.Epsilon) + continue; + var u = new Vector(edge.X / length, edge.Y / length); + var v = new Vector(-u.Y, u.X); + double minU = double.MaxValue, maxU = double.MinValue, minV = double.MaxValue, maxV = double.MinValue; + for (var j = 0; j < n; j++) + { + var pu = hull[j].X * u.X + hull[j].Y * u.Y; + var pv = hull[j].X * v.X + hull[j].Y * v.Y; + minU = System.Math.Min(minU, pu); + maxU = System.Math.Max(maxU, pu); + minV = System.Math.Min(minV, pv); + maxV = System.Math.Max(maxV, pv); + } + candidates.Add(new BoundingRectangleResult(System.Math.Atan2(edge.Y, edge.X), maxU - minU, maxV - minV)); + } + if (candidates.Count == 0) + return RotatingCalipers.MinimumBoundingRectangle(outline.ToList()); + + var least = candidates.Min(c => c.Area); + var slack = Math.Tolerance.Epsilon * System.Math.Max(least, 1); + return candidates.Where(c => c.Area <= least + slack) + .MinBy(c => System.Math.Max(c.Width, c.Height)); + } // A point `clear` from the nearest inside corner keeps `clearance` from it; with no // clearance it must still not be the corner.