diff --git a/OpenNest.Core/Support/SupportAnalyzer.cs b/OpenNest.Core/Support/SupportAnalyzer.cs index 4bcca2f2..7ec44af0 100644 --- a/OpenNest.Core/Support/SupportAnalyzer.cs +++ b/OpenNest.Core/Support/SupportAnalyzer.cs @@ -67,16 +67,24 @@ namespace OpenNest.Support samples = allowed; var scored = samples.Select(s => (s.Point, s.Clear, Margin: MarginWith(center, hulls, s.Point))).ToList(); - // Equally good points run along a straight edge; of those, the one farthest from an - // inside corner, or the first. + // A tab cannot lift the margin past the contacts' own hull edges, so a whole stretch + // of outline often ties; of those, the one nearest the line through the center across + // the outline's long side, then the one farthest from an inside corner, or the first. var bestMargin = scored.Count == 0 ? double.NegativeInfinity : scored.Max(s => s.Margin); + var along = scored.Count == 0 ? Vector.Zero : LongSide(material.Outline); Vector? best = null; + var bestOffset = double.PositiveInfinity; var bestClear = double.NegativeInfinity; foreach (var sample in scored) { - if (sample.Margin >= bestMargin - Math.Tolerance.Epsilon && sample.Clear > bestClear) + if (sample.Margin < bestMargin - Math.Tolerance.Epsilon) + continue; + var offset = System.Math.Abs((sample.Point - center).DotProduct(along)); + if (offset < bestOffset - Math.Tolerance.Epsilon + || (offset <= bestOffset + Math.Tolerance.Epsilon && sample.Clear > bestClear)) { best = sample.Point; + bestOffset = offset; bestClear = sample.Clear; } } @@ -85,6 +93,15 @@ namespace OpenNest.Support corners, insideCornerClearance); } + /// The unit direction of the longer side of the outline's minimum-area bounding rectangle. + private static Vector LongSide(IReadOnlyList outline) + { + // The full angle range checks every hull edge. + var box = RotatingCalipers.MinimumBoundingRectangle(ConvexHull.Compute(outline.ToList()), 0, Math.Angle.TwoPI); + var angle = box.Width >= box.Height ? box.Angle : box.Angle + Math.Angle.HalfPI; + return new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); + } + // A point `clear` from the nearest inside corner keeps `clearance` from it; with no // clearance it must still not be the corner. internal static bool IsClear(double clear, double clearance) => diff --git a/OpenNest.Tests/Support/SupportAnalyzerTests.cs b/OpenNest.Tests/Support/SupportAnalyzerTests.cs index f876e099..f523cb05 100644 --- a/OpenNest.Tests/Support/SupportAnalyzerTests.cs +++ b/OpenNest.Tests/Support/SupportAnalyzerTests.cs @@ -73,6 +73,39 @@ public class SupportAnalyzerTests Assert.True(support.MarginWith(new Vector(5, 0)) < 0); } + [Fact] + public void Strip_WhoseTabsTieAlongTheFarEdge_IsTabbedAcrossItsCenterOfGravity() + { + // The row's own edge caps the margin at 0.25, so tabs from about x = 2.5 to 7.5 on the + // far edge tie; the one across the center of gravity is chosen. + var grid = new SupportGrid(2, 4, 0, 0.25, stabilityMargin: 0.1); + + var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 1)); + + Assert.Equal(0.25, support.MarginWith(new Vector(3, 1)), 9); + Assert.Equal(0.25, support.MarginWith(new Vector(7, 1)), 9); + Assert.Equal(0.25, support.BestTabMargin, 9); + Assert.Equal(1, support.BestTabPoint!.Value.Y, 9); + Assert.InRange(support.BestTabPoint!.Value.X, 4.95, 5.05); + } + + [Fact] + public void Strip_RestingOnlyNearOneEnd_IsTabbedAtTheFarEndNotAcrossItsCenter() + { + // Two contacts at x = 1 hold a 10 x 2 strip whose center of gravity is at (5, 1); only a + // tab at the far end reaches past it, so the tie-break does not pull it to mid-side. + var grid = new SupportGrid(20, 1, 1, 0.5, stabilityMargin: 0.1); + + var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 2)); + + Assert.Equal(-4, support.Margin, 9); + Assert.True(support.TabCanSupport); + Assert.Equal(10, support.BestTabPoint!.Value.X, 9); + Assert.InRange(support.BestTabPoint!.Value.Y, 0.9, 1.1); + Assert.True(support.MarginWith(new Vector(5, 2)) < 0); + Assert.False(support.AcceptsTabAt(new Vector(5, 2))); + } + [Fact] public void Strip_BetweenRows_FallsThroughAndOnlyTheNearestTabIsAccepted() { diff --git a/docs/support-tabs.md b/docs/support-tabs.md index 7834cc90..b778597e 100644 --- a/docs/support-tabs.md +++ b/docs/support-tabs.md @@ -40,7 +40,11 @@ supported when its worst margin is positive and at least the stability margin. A perimeter tab holds the part at the tab, so a tab point counts as one more contact. `PartSupport.MarginWith(point)` is the worst margin with a tab there, and `BestTabPoint` the -outline point (vertices plus 256 even samples) that maximizes it. `AcceptsTabAt(point)` accepts +outline point (vertices plus 256 even samples) that maximizes it. A tab cannot lift the margin +past the contacts' own hull edges, so a whole stretch of outline often ties; among samples +within `Tolerance.Epsilon` of the best, the one nearest the line through the center of gravity +across the long side of the outline's minimum-area bounding rectangle wins. The search itself +is not narrowed, so a part held only near one end is still tabbed at the far end. `AcceptsTabAt(point)` accepts any point that reaches the stability margin, or, when no single tab can, only points as good as the best one found. A tab's bending strength is ignored: it is a support point, not a cantilever. @@ -48,9 +52,9 @@ A tab never goes in an inside corner, where its nub cannot be ground flush. An i a run of consecutive inward-turning outline vertices that turn at least 30 degrees together, so a chorded fillet counts while a shallow bend does not. Tab points keep the analysis's inside-corner clearance from those vertices (Plan Cutting and the overlay use twice the tab size; 0 keeps a tab -off the corner only), and `AcceptsTabAt` rejects closer points. Equally good points usually run -along a straight edge; of those, `BestTabPoint` is the one farthest from an inside corner. Only -when every outline point is too close does the clearance give way. +off the corner only), and `AcceptsTabAt` rejects closer points. The tie-break above runs over +the clear points only; points equally near the center line go to the one farthest from an +inside corner. Only when every outline point is too close does the clearance give way. View > Draw Support shows the grid (while its points are at least 6 pixels apart), each part's contacts, and its center of gravity: green when supported, red when it can tip, with an orange