feat(support): let a tab alone hold short parts the bed can't

Small parts such as 1.5" test squares on a 2" laser grid rest on at most
one grid point, so no single support tab can bring their center of
gravity inside the contacts and Plan Cutting warned "no single tab can
steady it" on every one. In practice a part that short stays in the
sheet on its tab, so the bed's contacts don't matter.

Cutting Settings > Tabs gets "Tab alone holds parts up to N long"
(CuttingParameters.TabHeldMaxLength, 0 = off, the default). On a plate
with a bed, a part the bed can't hold level whose length (long side of
its minimum-area bounding rectangle) is within the limit is still
tabbed, but any tab clear of inside corners steadies it, so Plan
Cutting, hand-placed lead-ins and the pre-post review warn only when it
has no tab at all. The setting persists with the cutting parameters and
is part of the plan state fingerprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-26 13:20:03 -04:00
1 parent a3a75d589d
commit 69a83b3ad9
15 files changed
+299 -22

No files matched your search

@@ -44,6 +44,7 @@ internal static class OwnedCuttingParameters
SupportTabs = source.SupportTabs,
NoBedTabMinWidth = Dimension(source.NoBedTabMinWidth),
NoBedTabMaxWidth = Dimension(source.NoBedTabMaxWidth),
TabHeldMaxLength = Dimension(source.TabHeldMaxLength),
Sequencing = new SequenceParameters
{
Method = sequence.Method,
@@ -92,6 +92,7 @@ internal static class StateFingerprint
Flag(p.SupportTabs);
Number(p.NoBedTabMinWidth);
Number(p.NoBedTabMaxWidth);
Number(p.TabHeldMaxLength);
Sequence(p.Sequencing);
Assignment(p.Assignment);
text.Append('}');
@@ -77,6 +77,19 @@ namespace OpenNest.CNC.CuttingStrategy
&& width >= NoBedTabMinWidth - Math.Tolerance.Epsilon
&& width <= NoBedTabMaxWidth + Math.Tolerance.Epsilon;
/// <summary>
/// With <see cref="SupportTabs"/> on a plate with a support bed, the longest part (the
/// long side of its outline's minimum-area bounding rectangle) its tab alone holds: such a
/// part the bed cannot hold level is tabbed, and any tab clear of inside corners steadies
/// it. 0 turns the rule off.
/// </summary>
public double TabHeldMaxLength { get; set; }
/// <summary>Whether a part <paramref name="length"/> long is held by its tab alone (see <see cref="TabHeldMaxLength"/>).</summary>
public bool TabHoldsAt(double length) =>
TabHeldMaxLength > 0 && double.IsFinite(TabHeldMaxLength)
&& length <= TabHeldMaxLength + Math.Tolerance.Epsilon;
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
}
@@ -103,10 +103,14 @@ internal static class PartSupportReview
private static string Message(PartSupport support, Vector? tab)
{
// Without a bed any tab steadies the part, so the only failure is no tab at all.
if (!support.HasBed)
return FormattableString.Invariant(
$"The part is {support.Width:0.####} wide, in the width range tabbed without a support bed, ")
// Without a bed, or for a part its tab alone holds, any tab steadies the part, so the only
// failure is no tab at all.
if (!support.HasBed || support.IsHeldByTab)
return (support.HasBed
? FormattableString.Invariant(
$"The part is {support.Length:0.####} long, short enough for its tab alone to hold, ")
: FormattableString.Invariant(
$"The part is {support.Width:0.####} wide, in the width range tabbed without a support bed, "))
+ "and it has no tab; it may tip once cut free."
+ (support.BestTabPoint is { } suggested
? FormattableString.Invariant($" Tab it near ({suggested.X:0.####}, {suggested.Y:0.####}).")
+61 -1
View File
@@ -104,6 +104,37 @@ namespace OpenNest.Support
return System.Math.Min(box.Width, box.Height);
}
/// <summary>
/// The part's length: the long side of its outline's minimum-area bounding rectangle, or
/// NaN when its material could not be read.
/// </summary>
public static double Length(SupportMaterial material)
{
ArgumentNullException.ThrowIfNull(material);
if (!material.IsComplete || material.Outline.Count < 3)
return double.NaN;
var box = BoundingRectangle(material.Outline);
return System.Math.Max(box.Width, box.Height);
}
/// <summary>
/// <paramref name="support"/>, a bed analysis of a part the bed cannot hold level, for a
/// part short enough for its tab alone to hold
/// (<see cref="CNC.CuttingStrategy.CuttingParameters.TabHoldsAt"/>): its contacts and
/// margin are kept, any tab clear of inside corners steadies it, and the best tab point is
/// chosen as without a bed (<see cref="AnalyzeWithoutBed"/>).
/// </summary>
public static PartSupport HeldByTab(PartSupport support, SupportMaterial material)
{
ArgumentNullException.ThrowIfNull(support);
ArgumentNullException.ThrowIfNull(material);
if (!support.IsComplete || support.IsSupported || !support.HasBed)
return support;
var best = BestTabPoint(material.Outline, support.CenterOfGravity, support.InsideCorners,
support.InsideCornerClearance, _ => double.PositiveInfinity, out _);
return support.WithTabHold(best, Length(material));
}
// The outline point a tab steadies the part most from, by `score`, clear of every inside
// corner where the outline allows it. 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
@@ -377,6 +408,25 @@ namespace OpenNest.Support
// A part on a plate with no bed, tabbed for its width: it rests on nothing modeled, and a
// tab anywhere clear of inside corners is as good as any other.
// A copy of `source` held by its tab alone: it keeps its contacts and margin, and any tab
// clear of inside corners steadies it.
private PartSupport(PartSupport source, Vector? bestTabPoint, double length)
{
CenterOfGravity = source.CenterOfGravity;
Contacts = source.Contacts;
Margin = source.Margin;
RequiredMargin = source.RequiredMargin;
hulls = source.hulls;
InsideCorners = source.InsideCorners;
InsideCornerClearance = source.InsideCornerClearance;
BestTabPoint = bestTabPoint;
BestTabMargin = double.PositiveInfinity;
IsHeldByTab = true;
Length = length;
}
internal PartSupport WithTabHold(Vector? bestTabPoint, double length) => new(this, bestTabPoint, length);
internal PartSupport(Vector centerOfGravity, Vector? bestTabPoint, IReadOnlyList<Vector> insideCorners,
double insideCornerClearance, double width)
{
@@ -397,6 +447,16 @@ namespace OpenNest.Support
/// </summary>
public bool HasBed { get; } = true;
/// <summary>
/// True for a part on a bed that cannot hold it level but short enough for its tab alone
/// to hold (<see cref="SupportAnalyzer.HeldByTab"/>): any tab clear of inside corners
/// steadies it.
/// </summary>
public bool IsHeldByTab { get; }
/// <summary>The part's length when <see cref="IsHeldByTab"/> (see <see cref="SupportAnalyzer.Length"/>); NaN otherwise.</summary>
public double Length { get; } = double.NaN;
/// <summary>The part's width when analyzed without a bed (see <see cref="SupportAnalyzer.Width"/>); NaN otherwise.</summary>
public double Width { get; } = double.NaN;
@@ -440,7 +500,7 @@ namespace OpenNest.Support
/// <summary>The worst margin with a tab holding the part at <paramref name="tab"/>.</summary>
public double MarginWith(Vector tab) =>
!IsComplete ? double.NegativeInfinity
: !HasBed ? double.PositiveInfinity
: !HasBed || IsHeldByTab ? double.PositiveInfinity
: SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab);
/// <summary>True when a tab at <paramref name="tab"/> holds the part level by the required margin.</summary>
+10 -2
View File
@@ -15,7 +15,9 @@ namespace OpenNest.Support
{
/// <summary>
/// The analysis that tabs a part, or null when support tabs are off or the part is
/// outside the rule: with a bed, the bed's analysis; without one, a no-bed analysis
/// outside the rule: with a bed, the bed's analysis, held by its tab alone
/// (<see cref="SupportAnalyzer.HeldByTab"/>) when the bed cannot hold it level and its
/// length is within <see cref="CuttingParameters.TabHoldsAt"/>; without one, a no-bed analysis
/// (<see cref="SupportAnalyzer.AnalyzeWithoutBed"/>) of a part whose width is in
/// <see cref="CuttingParameters.TabsWithoutBedAt"/>'s range. The part needs a tab when
/// the result is complete and not supported; an incomplete result could not be read.
@@ -30,7 +32,13 @@ namespace OpenNest.Support
if (parameters?.SupportTabs != true)
return null;
if (bed != null)
return SupportAnalyzer.Analyze(bed, quadrant, material, insideCornerClearance);
{
var support = SupportAnalyzer.Analyze(bed, quadrant, material, insideCornerClearance);
return support is { IsComplete: true, IsSupported: false }
&& parameters.TabHoldsAt(SupportAnalyzer.Length(material))
? SupportAnalyzer.HeldByTab(support, material)
: support;
}
if (!parameters.HasNoBedTabRange())
return null;
if (!material.IsComplete)
@@ -81,6 +81,8 @@ internal static class SupportTabCapture
var part = source.SourceOrdinal + 1;
var held = !support.HasBed
? FormattableString.Invariant($"is {support.Width:0.####} wide, in the width range tabbed without a support bed,")
: support.IsHeldByTab
? FormattableString.Invariant($"is {support.Length:0.####} long, short enough for its tab alone to hold,")
: support.Margin == double.NegativeInfinity
? "rests on no support contact"
: support.Margin < 0
@@ -93,7 +95,7 @@ internal static class SupportTabCapture
: support.TabCanSupport
? "no lead-in fits at a point where its tab would steady the part"
: "no single tab can steady it";
var needs = support.HasBed ? FormattableString.Invariant($" (needs {support.RequiredMargin:0.####})") : "";
var needs = support.HasBed && !support.IsHeldByTab ? FormattableString.Invariant($" (needs {support.RequiredMargin:0.####})") : "";
return new CuttingPlanFinding(source.SourceOrdinal, source.SourcePart, null, null,
PostVerificationKind.PartSupport, $"Part {part} {held}{needs} and {why}; it may tip once cut free.");
}
@@ -155,4 +155,17 @@ public class SupportGridPersistenceTests
Assert.Equal(4, snapshot.NoBedTabMaxWidth);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").NoBedTabMaxWidth);
}
[Fact]
public void TabHeldLengthRoundTripsInCuttingParameters()
{
var parameters = new CuttingParameters { SupportTabs = true, TabHeldMaxLength = 2 };
var restored = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
var snapshot = CuttingParametersSerializer.FromDto(CuttingParametersSerializer.ToDto(parameters));
Assert.Equal(2, restored.TabHeldMaxLength);
Assert.Equal(2, snapshot.TabHeldMaxLength);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").TabHeldMaxLength);
}
}
+1
View File
@@ -35,6 +35,7 @@ public class CuttingParametersDto
public bool SupportTabs { get; set; }
public double NoBedTabMinWidth { get; set; }
public double NoBedTabMaxWidth { get; set; }
public double TabHeldMaxLength { get; set; }
public SequenceDto Sequencing { get; set; } = new();
public AssignmentDto Assignment { get; set; } = new();
@@ -73,6 +73,7 @@ public static class CuttingParametersSerializer
SupportTabs = parameters.SupportTabs,
NoBedTabMinWidth = parameters.NoBedTabMinWidth,
NoBedTabMaxWidth = parameters.NoBedTabMaxWidth,
TabHeldMaxLength = parameters.TabHeldMaxLength,
Sequencing = ToSequenceDto(parameters.Sequencing),
Assignment = ToAssignmentDto(parameters.Assignment),
};
@@ -108,6 +109,7 @@ public static class CuttingParametersSerializer
SupportTabs = dto.SupportTabs,
NoBedTabMinWidth = dto.NoBedTabMinWidth,
NoBedTabMaxWidth = dto.NoBedTabMaxWidth,
TabHeldMaxLength = dto.TabHeldMaxLength,
Sequencing = FromSequenceDto(dto.Sequencing),
Assignment = FromAssignmentDto(dto.Assignment),
};
@@ -44,6 +44,7 @@ public class OwnedCuttingParameterTests
SupportTabs = true,
NoBedTabMinWidth = 0.5,
NoBedTabMaxWidth = 4,
TabHeldMaxLength = 2,
RoundLeadInAngles = true,
LeadInAngleIncrement = 30,
Sequencing = new SequenceParameters
@@ -0,0 +1,140 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Support;
using OpenNest.Tests.CuttingPlanning;
namespace OpenNest.Tests.Support;
/// <summary>
/// Support tabs on a bed for parts short enough for their tab alone to hold
/// (<see cref="CuttingParameters.TabHeldMaxLength"/>): a part the bed cannot hold level is still
/// tabbed, but any tab clear of inside corners steadies it, so it is not reported.
/// </summary>
public class TabHeldSupportTabTests
{
private const double TabSize = 0.2;
// A 2 inch laser grid; a 1.5 inch square rests on one of its points at most.
private static readonly SupportGrid Bed = new(2, 2, 0, 0, registrationTolerance: 0.125, stabilityMargin: 0.25);
private static CuttingParameters Parameters(double tabHeldMaxLength)
{
var parameters = ExplicitContourTests.Parameters();
parameters.SupportTabs = true;
parameters.TabConfig = new NormalTab { Size = TabSize };
parameters.TabHeldMaxLength = tabHeldMaxLength;
return parameters;
}
// A square `size` on a side at (3, 3) on the 2 inch grid.
private static (Nest Nest, Plate Plate, Part Part) Square(CuttingParameters parameters, double size = 1.5)
{
var nest = new Nest("tab held");
var plate = new Plate(20, 20) { CuttingParameters = parameters };
nest.Plates.Add(plate);
nest.SetSupportBed(Bed);
var part = new Part(new Drawing("square", LeadPathValidationTests.Rectangle(0, 0, size, size)), new Vector(3, 3));
plate.Parts.Add(part);
return (nest, plate, part);
}
private static double TabGap(FixedProgramPlacement proposal)
{
var motions = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
var entry = motions.Last(m => m.Layer == LayerType.Leadin).End;
var cutEnd = motions.Last(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display).End;
return entry.DistanceTo(cutEnd);
}
[Theory]
[InlineData(0, false)] // off
[InlineData(1, false)]
[InlineData(1.5, true)] // the bound is inclusive
[InlineData(2, true)]
public void TheLimitHoldsPartsUpToIt(double max, bool holdsALengthOfOneAndAHalf)
{
Assert.Equal(holdsALengthOfOneAndAHalf, new CuttingParameters { TabHeldMaxLength = max }.TabHoldsAt(1.5));
}
[Fact]
public void Analyze_HoldsAShortPartTheBedCannotByAnyClearTab()
{
var (_, _, part) = Square(Parameters(2));
var material = SupportMaterial.FromPart(part);
var held = SupportTabSettings.Analyze(Parameters(2), Bed, 1, material);
var bedOnly = SupportTabSettings.Analyze(Parameters(0), Bed, 1, material);
Assert.Equal(1.5, SupportAnalyzer.Length(material), 6);
Assert.False(bedOnly.IsHeldByTab);
Assert.False(bedOnly.TabCanSupport);
Assert.True(held.IsHeldByTab);
Assert.True(held.HasBed);
Assert.False(held.IsSupported);
Assert.Equal(bedOnly.Margin, held.Margin);
Assert.True(held.TabCanSupport);
Assert.True(held.AcceptsTabAt(new Vector(3, 3.75)));
Assert.True(held.IsSteadiedByTabAt(new Vector(4.5, 3.75)));
Assert.False(SupportTabSettings.Analyze(Parameters(1), Bed, 1, material).IsHeldByTab);
}
[Fact]
public void Analyze_LeavesAPartTheBedHoldsAlone()
{
// A 5 inch square rests on a 2 x 2 block of grid points around its center.
var (_, _, part) = Square(Parameters(6), 5);
var support = SupportTabSettings.Analyze(Parameters(6), Bed, 1, SupportMaterial.FromPart(part));
Assert.True(support.IsSupported);
Assert.False(support.IsHeldByTab);
}
[Fact]
public void PlanCutting_WarnsAboutAShortPartNoTabCanSteady_WithoutTheLimit()
{
var (_, plate, _) = Square(Parameters(0));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(0)));
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
Assert.Contains("no single tab can steady it", Assert.Single(result.Findings).Message);
}
[Fact]
public void PlanCutting_TabsAShortPartWithoutAWarning()
{
var (_, plate, _) = Square(Parameters(2));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(2)));
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
Assert.Equal(TabSize, TabGap(Assert.Single(result.ProposedOrder)), 6);
Assert.Empty(result.Findings);
}
[Fact]
public void PrePost_WarnsOnlyAboutAnUntabbedShortPart()
{
var lead = new Line(new Vector(0, 0), new Vector(0, 1.5));
var (untabbed, _, bare) = Square(Parameters(2));
bare.ApplySingleLeadIn(Parameters(2), new Vector(0, 0.75), lead, ContourType.External);
var (tabbed, _, held) = Square(Parameters(2));
var withTabs = Parameters(2);
withTabs.TabsEnabled = true;
held.ApplySingleLeadIn(withTabs, new Vector(0, 0.75), lead, ContourType.External);
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(untabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("short enough for its tab alone to hold", finding.Message);
Assert.Contains("no tab", finding.Message);
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
}
}
+28 -12
View File
@@ -45,9 +45,10 @@ namespace OpenNest.Controls
private readonly NumericUpDown nudTabWidth;
private readonly RadioButton rbTabAll;
private readonly RadioButton rbSupportTab;
private readonly TableLayoutPanel noBedOptions;
private readonly TableLayoutPanel supportTabOptions;
private readonly NumericUpDown nudNoBedTabMin;
private readonly NumericUpDown nudNoBedTabMax;
private readonly NumericUpDown nudTabHeldMax;
private readonly NumericUpDown nudPierceClearance;
private readonly NumericUpDown nudPierceDiameter;
@@ -194,23 +195,35 @@ namespace OpenNest.Controls
"Plan Cutting tabs each part that would tip once cut free, where it best keeps the part level.");
AddSpanningRow(tabOptions, rbSupportTab);
// A plate with no support bed tabs the parts whose narrow side is in this range;
// a maximum of 0 tabs none.
noBedOptions = new TableLayoutPanel
// A part up to this long is held by its tab alone, wherever the tab goes; a plate with
// no support bed tabs the parts whose narrow side is in the range below. A maximum of
// 0 turns either rule off.
supportTabOptions = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Margin = new Padding(FieldIndent + 2, 0, 0, 0),
};
noBedOptions.Controls.Add(
nudTabHeldMax = CreateNumeric(0, 0.25);
nudTabHeldMax.Width = 80;
toolTip.SetToolTip(nudTabHeldMax,
"Parts this short stay in the sheet on their tab, so a tab anywhere holds them without the grid.");
supportTabOptions.Controls.Add(
CreateInline(
CreateHint("Tab alone holds parts up to", new Padding(0, 0, 4, 0)),
nudTabHeldMax,
CreateHint("long (0 = off)", new Padding(6, 0, 0, 0))
)
);
supportTabOptions.Controls.Add(
CreateHint("On plates with no support bed, tab parts whose narrow side is:", new Padding(0, 4, 0, 2))
);
nudNoBedTabMin = CreateNumeric(0, 0.25);
nudNoBedTabMin.Width = 80;
nudNoBedTabMax = CreateNumeric(0, 0.25);
nudNoBedTabMax.Width = 80;
noBedOptions.Controls.Add(
supportTabOptions.Controls.Add(
CreateInline(
nudNoBedTabMin,
CreateHint("to", new Padding(4, 0, 4, 0)),
@@ -218,25 +231,26 @@ namespace OpenNest.Controls
CreateHint("(0 = none)", new Padding(6, 0, 0, 0))
)
);
AddSpanningRow(tabOptions, noBedOptions);
AddSpanningRow(tabOptions, supportTabOptions);
layout.Controls.Add(tabOptions);
chkTabsEnabled.CheckedChanged += (s, e) =>
{
tabOptions.Enabled = chkTabsEnabled.Checked;
UpdateNoBedRange();
UpdateSupportTabOptions();
OnParametersChanged();
};
// The two choices toggle together, so one handler sees every change.
rbTabAll.CheckedChanged += (s, e) =>
{
UpdateNoBedRange();
UpdateSupportTabOptions();
OnParametersChanged();
};
nudTabWidth.ValueChanged += (s, e) => OnParametersChanged();
nudNoBedTabMin.ValueChanged += (s, e) => OnParametersChanged();
nudNoBedTabMax.ValueChanged += (s, e) => OnParametersChanged();
nudTabHeldMax.ValueChanged += (s, e) => OnParametersChanged();
// Pierce
AddSection("Pierce");
@@ -289,6 +303,7 @@ namespace OpenNest.Controls
SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked,
NoBedTabMinWidth = (double)nudNoBedTabMin.Value,
NoBedTabMaxWidth = (double)nudNoBedTabMax.Value,
TabHeldMaxLength = (double)nudTabHeldMax.Value,
};
}
@@ -308,8 +323,9 @@ namespace OpenNest.Controls
nudTabWidth.Value = Clamp(nudTabWidth, p.TabConfig.Size);
nudNoBedTabMin.Value = Clamp(nudNoBedTabMin, p.NoBedTabMinWidth);
nudNoBedTabMax.Value = Clamp(nudNoBedTabMax, p.NoBedTabMaxWidth);
nudTabHeldMax.Value = Clamp(nudTabHeldMax, p.TabHeldMaxLength);
tabOptions.Enabled = chkTabsEnabled.Checked;
UpdateNoBedRange();
UpdateSupportTabOptions();
nudPierceClearance.Value = Clamp(nudPierceClearance, p.PierceClearance);
nudPierceDiameter.Value = Clamp(nudPierceDiameter, p.PierceDiameter);
chkRoundLeadInAngles.Checked = p.RoundLeadInAngles;
@@ -319,9 +335,9 @@ namespace OpenNest.Controls
suppressEvents = false;
}
private void UpdateNoBedRange()
private void UpdateSupportTabOptions()
{
noBedOptions.Enabled = chkTabsEnabled.Checked && rbSupportTab.Checked;
supportTabOptions.Enabled = chkTabsEnabled.Checked && rbSupportTab.Checked;
}
private static decimal Clamp(NumericUpDown control, double value) =>
+2 -1
View File
@@ -39,7 +39,8 @@ var result = CuttingPlanService.Plan(snapshot, cancellationToken);
the plate's own under `ForPlate`), each regenerated part the bed cannot hold level gets a
tabbed perimeter whose automatic entries keep to points where the tab steadies it. A tab
that still cannot hold its part level, or sits in an inside corner, is noted on the plan
without changing its status. Without a bed, the parts whose width is in
without changing its status. A part no longer than `TabHeldMaxLength` is held by its tab
alone, so any tab clear of inside corners steadies it. Without a bed, the parts whose width is in
`NoBedTabMinWidth`..`NoBedTabMaxWidth` are tabbed instead, and with no range set the plan notes
that nothing was tabbed; see [support tabs](support-tabs.md).
- Parameters are caller-confirmed inputs. The service does not recover missing
+15 -1
View File
@@ -90,6 +90,20 @@ A support-tabbed perimeter's automatic entries are therefore limited to catalogu
analysis accepts, plus the closest native point to its best tab point (`AutomaticEntryKind.SupportTab`,
never on a reflex or cusp vertex). Ranking and lead checks are unchanged.
## Parts the tab alone holds
A tabbed part stays in the sheet on its tab, and a part short enough does not need the bed at
all: the tab holds it wherever it goes. Cutting Settings > Tabs > "Tab alone holds parts up to"
sets `CuttingParameters.TabHeldMaxLength`: on a plate with a support bed, a part the bed cannot
hold level whose length (the long side of its outline's minimum-area bounding rectangle,
`SupportAnalyzer.Length`) is at most that, bound included, is still tabbed, but
`SupportAnalyzer.HeldByTab` marks its analysis `IsHeldByTab`: it keeps its contacts and margin,
every tab clear of inside corners steadies it, and its best tab point is chosen as on a plate
with no bed (below). A part the bed holds is not tabbed. 0 turns the rule off.
`SupportTabSettings.Analyze` applies it for Plan Cutting, hand-placed lead-ins and the pre-post
review alike, so such a part warns only when it is cut with no tab at all. View > Draw Support
does not read cutting settings and still shows it red, since it would tip without its tab.
## Plates without a support bed
With no bed there are no contacts to judge a part by. Cutting Settings > Tabs > "No bed: tab
@@ -142,7 +156,7 @@ cannot hold it level or, with no bed, its width is in the no-bed range) is check
program was made. The tab is the gap between
the end and the start of the program's outside cut (the chain of cutting motions with the
largest extent). A part with no such gap, or whose gap does not steady it (without
a bed any gap does), gets a `PartSupport` warning at its center of gravity naming the best tab point; posting needs
a bed, or for a part its tab alone holds, any gap does), gets a `PartSupport` warning at its center of gravity naming the best tab point; posting needs
consent as for every warning. A part whose material cannot be read is reported as not checked.
Not covered yet: