fix(support): center tied support tabs across the part's long side

A tab cannot lift a part's margin past its contacts' own hull edges, so a
whole stretch of outline often ties for the best tab point, and the first
sample found won, leaving the tab off-center. Keep the full outline search
for the margin, then among samples within Tolerance.Epsilon of the best,
pick the one nearest the line through the center of gravity across the
long side of the outline's minimum-area bounding rectangle. A part held
only near one end is still tabbed at the far end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-20 23:39:05 -04:00
1 parent 7bf753ce17
commit c3881c5534
3 files changed
+61 -7

No files matched your search

+20 -3
View File
@@ -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);
}
/// <summary>The unit direction of the longer side of the outline's minimum-area bounding rectangle.</summary>
private static Vector LongSide(IReadOnlyList<Vector> 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) =>
@@ -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()
{
+8 -4
View File
@@ -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