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