feat(tabs): replace "Tab every part" with tabbing parts by size

Nobody tabs every part, but tabbing by size is useful even on a plate
that has a support grid. Cutting Settings > Tabs now offers "Only tab
parts the support grid can't hold level" or "Tab parts by size"
(CuttingParameters.TabBySize), which ignores the bed and tabs each part
whose minimum-area bounding rectangle's narrow side is in the range.
The support grid choice still falls back to that range on plates with
no bed, as before.

The range fields become TabSizeMinWidth/TabSizeMaxWidth; their JSON
keys stay noBedTabMinWidth/noBedTabMaxWidth so saved settings and nests
read unchanged. TabsEnabled stays as the per-contour flag Plan Cutting,
Place Lead-in and drawing pins set on the parts they tab.

Saved user settings with "Tab every part" read as tabs by size from 0
to 9999, which tabs the same parts. Only the user's settings migrate;
a plate's recorded parameters and pins replay as planned.

Plan Cutting, Place Lead-in and the pre-post review all go through
SupportTabSettings, so they agree on which parts need a tab. A
size-tabbed part is held by any clear tab, so it is never a rapid
obstacle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-05 07:46:24 -05:00
1 parent ae14458444
commit 6de23fb831
20 files changed
+503 -138

No files matched your search

@@ -42,8 +42,9 @@ internal static class OwnedCuttingParameters
: CopyTab(source.TabConfig),
TabsEnabled = source.TabsEnabled,
SupportTabs = source.SupportTabs,
NoBedTabMinWidth = Dimension(source.NoBedTabMinWidth),
NoBedTabMaxWidth = Dimension(source.NoBedTabMaxWidth),
TabBySize = source.TabBySize,
TabSizeMinWidth = Dimension(source.TabSizeMinWidth),
TabSizeMaxWidth = Dimension(source.TabSizeMaxWidth),
TabHeldMaxLength = Dimension(source.TabHeldMaxLength),
Sequencing = new SequenceParameters
{
@@ -90,8 +90,9 @@ internal static class StateFingerprint
TabSettings(p.TabConfig);
Flag(p.TabsEnabled);
Flag(p.SupportTabs);
Number(p.NoBedTabMinWidth);
Number(p.NoBedTabMaxWidth);
Flag(p.TabBySize);
Number(p.TabSizeMinWidth);
Number(p.TabSizeMaxWidth);
Number(p.TabHeldMaxLength);
Sequence(p.Sequencing);
Assignment(p.Assignment);
@@ -42,40 +42,55 @@ namespace OpenNest.CNC.CuttingStrategy
public double LeadInAngleIncrement { get; set; } = 5.0;
public Tab TabConfig { get; set; }
/// <summary>
/// Whether a contour is cut with a tab at its entry. The cutting settings never set it for
/// every part: Plan Cutting, Place Lead-in and drawing pins set it on the copies of the
/// settings they give the parts they tab. Parameters recorded with it on replay as planned.
/// </summary>
public bool TabsEnabled { get; set; }
/// <summary>
/// Tabs each part its plate's support bed cannot hold level once it is cut free, at the
/// perimeter point that best keeps its center of gravity over the bed's contacts (see
/// docs/support-tabs.md). Needs <see cref="TabConfig"/> for the tab size.
/// Tabs the parts that need one (see docs/support-tabs.md): with <see cref="TabBySize"/>
/// off, each part its plate's support bed cannot hold level once it is cut free, at the
/// perimeter point that best keeps its center of gravity over the bed's contacts, or on a
/// plate with no bed each part in the size range; with it on, each part in the size range.
/// Needs <see cref="TabConfig"/> for the tab size.
/// </summary>
public bool SupportTabs { get; set; }
/// <summary>
/// With <see cref="SupportTabs"/> on a plate that has no support bed, the narrowest part
/// width that is tabbed instead (see <see cref="TabsWithoutBedAt"/>).
/// With <see cref="SupportTabs"/>, ignore the support bed and tab every part whose width is
/// in <see cref="TabSizeMinWidth"/> to <see cref="TabSizeMaxWidth"/>
/// (<see cref="TabsBySizeAt"/>).
/// </summary>
public double NoBedTabMinWidth { get; set; }
public bool TabBySize { get; set; }
/// <summary>
/// With <see cref="SupportTabs"/> on a plate that has no support bed, the widest part
/// width that is tabbed instead; 0 turns the rule off, so nothing is tabbed without a bed.
/// The narrowest part width that is tabbed by size: with <see cref="TabBySize"/>, or with
/// <see cref="SupportTabs"/> on a plate that has no support bed (see <see cref="TabsBySizeAt"/>).
/// </summary>
public double NoBedTabMaxWidth { get; set; }
public double TabSizeMinWidth { get; set; }
/// <summary>Whether <see cref="NoBedTabMinWidth"/> to <see cref="NoBedTabMaxWidth"/> is a usable range.</summary>
public bool HasNoBedTabRange() =>
NoBedTabMaxWidth > 0 && double.IsFinite(NoBedTabMaxWidth)
&& NoBedTabMinWidth >= 0 && NoBedTabMinWidth <= NoBedTabMaxWidth;
/// <summary>
/// The widest part width that is tabbed by size; 0 turns the rule off, so nothing is
/// tabbed by size.
/// </summary>
public double TabSizeMaxWidth { get; set; }
/// <summary>Whether <see cref="TabSizeMinWidth"/> to <see cref="TabSizeMaxWidth"/> is a usable range.</summary>
public bool HasTabSizeRange() =>
TabSizeMaxWidth > 0 && double.IsFinite(TabSizeMaxWidth)
&& TabSizeMinWidth >= 0 && TabSizeMinWidth <= TabSizeMaxWidth;
/// <summary>
/// Whether a part <paramref name="width"/> wide (the short side of its minimum-area
/// bounding rectangle) is tabbed when its plate has no support bed.
/// bounding rectangle) is tabbed by size.
/// </summary>
public bool TabsWithoutBedAt(double width) =>
HasNoBedTabRange()
&& width >= NoBedTabMinWidth - Math.Tolerance.Epsilon
&& width <= NoBedTabMaxWidth + Math.Tolerance.Epsilon;
public bool TabsBySizeAt(double width) =>
HasTabSizeRange()
&& width >= TabSizeMinWidth - Math.Tolerance.Epsilon
&& width <= TabSizeMaxWidth + Math.Tolerance.Epsilon;
/// <summary>
/// With <see cref="SupportTabs"/> on a plate with a support bed, the longest part (the
@@ -12,9 +12,9 @@ namespace OpenNest.Diagnostics;
/// <summary>
/// Pre-post review of part support (docs/support-tabs.md). Every part whose cutting parameters
/// ask for support tabs and that needs one (<see cref="SupportTabSettings.Analyze"/>: its plate's
/// support bed cannot hold it level or, without a bed, its width is in the no-bed range) must be
/// support bed cannot hold it level or, by size, its width is in the size range) must be
/// cut with a tab that keeps it level, however its program was made (planned, placed by hand or
/// locked); without a bed any tab will do. The tab is the gap the outside cut leaves between its end and its start. Reports
/// locked); by size any tab will do. The tab is the gap the outside cut leaves between its end and its start. Reports
/// only; a part whose material or program cannot be read is reported as not checked.
/// </summary>
internal static class PartSupportReview
@@ -28,8 +28,7 @@ internal static class PartSupportReview
token.ThrowIfCancellationRequested();
var part = plate.Parts[index];
var parameters = part?.CuttingParameters ?? plate.CuttingParameters;
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff || parameters?.SupportTabs != true
|| (bed == null && !parameters.HasNoBedTabRange()))
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff || !SupportTabSettings.CanTab(parameters, bed))
continue;
var partNumber = index + 1;
try
@@ -110,7 +109,7 @@ internal static class PartSupportReview
? 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, "))
$"The part is {support.Width:0.####} wide, in the width range tabbed by size, "))
+ "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.####}).")
+1 -1
View File
@@ -67,7 +67,7 @@ namespace OpenNest.Support
/// <summary>
/// A part on a plate with no support bed, tabbed for its <see cref="Width"/> instead
/// (<see cref="CNC.CuttingStrategy.CuttingParameters.TabsWithoutBedAt"/>). With no contacts
/// (<see cref="CNC.CuttingStrategy.CuttingParameters.TabsBySizeAt"/>). With no contacts
/// to judge by, a tab anywhere clear of inside corners is accepted; the best tab point is
/// the clear outline point nearest the line through the center of gravity across the
/// outline's long side, then the one farthest from an inside corner.
+54 -15
View File
@@ -6,20 +6,41 @@ namespace OpenNest.Support
{
/// <summary>
/// Which parts <see cref="CuttingParameters.SupportTabs"/> tabs (docs/support-tabs.md): the
/// parts the plate's support bed cannot hold level or, on a plate with no bed, the parts
/// whose width is in the settings' no-bed range. Also the cutting settings for a lead-in
/// parts the plate's support bed cannot hold level or, by size (<see cref="CuttingParameters.TabBySize"/>,
/// or a plate with no bed), the parts whose width is in the settings' size range. Also the cutting settings for a lead-in
/// placed by hand: the tool cuts where it is clicked, so the part is tabbed there. Plan
/// Cutting chooses the tab point itself.
/// </summary>
public static class SupportTabSettings
{
/// <summary>
/// The widest part a migrated "Tab every part" setting tabs by size: wider than any sheet,
/// so the same parts stay tabbed. It is also the cutting panel's largest range value.
/// </summary>
public const double TabEveryPartMaxWidth = 9999;
/// <summary>
/// Whether support tabs judge parts by the plate's support bed: they are on, not by size,
/// and the plate has a bed. Otherwise they go by the size range.
/// </summary>
public static bool UsesBed(CuttingParameters parameters, SupportBed bed) =>
parameters?.SupportTabs == true && !parameters.TabBySize && bed != null;
/// <summary>
/// Whether support tabs are on and can tab a part on a plate with <paramref name="bed"/>:
/// by the bed, or by a usable size range.
/// </summary>
public static bool CanTab(CuttingParameters parameters, SupportBed bed) =>
parameters?.SupportTabs == true && (UsesBed(parameters, bed) || parameters.HasTabSizeRange());
/// <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, held by its tab alone
/// outside the rule: by the 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
/// length is within <see cref="CuttingParameters.TabHoldsAt"/>; by size (no bed, or
/// <see cref="CuttingParameters.TabBySize"/>), 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
/// <see cref="CuttingParameters.TabsBySizeAt"/>'s range. The part needs a tab when
/// the result is complete and not supported; an incomplete result could not be read.
/// </summary>
/// <param name="insideCornerClearance">
@@ -31,7 +52,7 @@ namespace OpenNest.Support
ArgumentNullException.ThrowIfNull(material);
if (parameters?.SupportTabs != true)
return null;
if (bed != null)
if (UsesBed(parameters, bed))
{
var support = SupportAnalyzer.Analyze(bed, quadrant, material, insideCornerClearance);
return support is { IsComplete: true, IsSupported: false }
@@ -39,21 +60,21 @@ namespace OpenNest.Support
? SupportAnalyzer.HeldByTab(support, material)
: support;
}
if (!parameters.HasNoBedTabRange())
if (!parameters.HasTabSizeRange())
return null;
if (!material.IsComplete)
return SupportAnalyzer.AnalyzeWithoutBed(material, insideCornerClearance);
return parameters.TabsWithoutBedAt(SupportAnalyzer.Width(material))
return parameters.TabsBySizeAt(SupportAnalyzer.Width(material))
? SupportAnalyzer.AnalyzeWithoutBed(material, insideCornerClearance)
: null;
}
/// <summary>
/// Whether support tabs are on but can tab nothing: the plate has no support bed and the
/// settings set no no-bed width range.
/// Whether support tabs are on but can tab nothing: they go by size (no bed, or
/// <see cref="CuttingParameters.TabBySize"/>) and the settings set no size range.
/// </summary>
public static bool SkipsWithoutBed(CuttingParameters parameters, SupportBed bed) =>
parameters?.SupportTabs == true && bed == null && !parameters.HasNoBedTabRange();
public static bool SkipsForNoRange(CuttingParameters parameters, SupportBed bed) =>
parameters?.SupportTabs == true && !CanTab(parameters, bed);
/// <summary>
/// <paramref name="parameters"/> itself, or a copy with tabs enabled when support tabs are
@@ -63,9 +84,8 @@ namespace OpenNest.Support
public static CuttingParameters ForManualLeadIn(CuttingParameters parameters, Plate plate, Part part)
{
ArgumentNullException.ThrowIfNull(parameters);
if (!parameters.SupportTabs || parameters.TabsEnabled || parameters.TabConfig == null
|| plate == null || part == null
|| (plate.SupportBed == null && !parameters.HasNoBedTabRange()))
if (parameters.TabsEnabled || parameters.TabConfig == null || plate == null || part == null
|| !CanTab(parameters, plate.SupportBed))
return parameters;
var support = Analyze(parameters, plate.SupportBed, plate.Quadrant, SupportMaterial.FromPart(part));
if (support is not { IsComplete: true, IsSupported: false })
@@ -74,5 +94,24 @@ namespace OpenNest.Support
tabbed.TabsEnabled = true;
return tabbed;
}
/// <summary>
/// Moves the user's cutting settings off the retired "Tab every part" choice
/// (<see cref="CuttingParameters.TabsEnabled"/> on with support tabs off): they become tabs
/// by size from 0 to <see cref="TabEveryPartMaxWidth"/>, which tabs the same parts. Returns
/// whether it changed <paramref name="parameters"/>. Only for the settings the user edits,
/// never a plate's recorded parameters or a drawing pin's, which replay as planned.
/// </summary>
public static bool MigrateTabEveryPart(CuttingParameters parameters)
{
if (parameters is not { TabsEnabled: true, SupportTabs: false })
return false;
parameters.TabsEnabled = false;
parameters.SupportTabs = true;
parameters.TabBySize = true;
parameters.TabSizeMinWidth = 0;
parameters.TabSizeMaxWidth = TabEveryPartMaxWidth;
return true;
}
}
}
@@ -123,8 +123,9 @@ public sealed class CuttingPlanSnapshot
internal IReadOnlyList<CuttingPlanFinding> Findings { get; }
/// <summary>
/// Support tabs are on, but the request had no support bed and its settings set no no-bed
/// width range, so capture tabbed no part; the plan carries a note saying so.
/// Support tabs are on but go by size (the request had no support bed, or its settings tab by
/// size) and its settings set no size range, so capture tabbed no part; the plan carries a
/// note saying so.
/// </summary>
internal bool SupportTabsSkipped { get; init; }
@@ -181,7 +181,7 @@ public static class CuttingPlanService
: OwnedCuttingParameters.Copy(request.ConfirmedParameters),
dependencies: dependencies, findings: warnings, bestEffort: bestEffort)
{
SupportTabsSkipped = SupportTabSettings.SkipsWithoutBed(request.ConfirmedParameters, request.SupportBed),
SupportTabsSkipped = SupportTabSettings.SkipsForNoRange(request.ConfirmedParameters, request.SupportBed),
};
}
catch (OperationCanceledException)
@@ -13,8 +13,8 @@ namespace OpenNest.Engine.CuttingPlanning;
/// <summary>
/// Capture-time support tabs for one regenerated part (docs/support-tabs.md): with
/// <see cref="CNC.CuttingStrategy.CuttingParameters.SupportTabs"/> on, a part the support bed
/// cannot hold level, or on a plate with no bed a part in the no-bed width range
/// (<see cref="SupportTabSettings.Analyze"/>), has its perimeter tabbed where the tab steadies
/// cannot hold level, or by size (no bed, or <see cref="CNC.CuttingStrategy.CuttingParameters.TabBySize"/>)
/// a part in the size range (<see cref="SupportTabSettings.Analyze"/>), has its perimeter tabbed where the tab steadies
/// it; a pinned perimeter
/// keeps its pin (the drawing wins) and only carries the analysis for replay to review. A
/// supported part, a missing tab configuration or material the analyzer cannot read leave the
@@ -27,8 +27,8 @@ internal static class SupportTabCapture
{
var parameters = request.ConfirmedParameters;
// A display perimeter is a supplied blank: it is never cut free, so it needs no tab.
if (prepared == null || !prepared.HasCutPerimeter || parameters?.SupportTabs != true
|| parameters.TabConfig == null || (request.SupportBed == null && !parameters.HasNoBedTabRange()))
if (prepared == null || !prepared.HasCutPerimeter || parameters?.TabConfig == null
|| !SupportTabSettings.CanTab(parameters, request.SupportBed))
return prepared;
token.ThrowIfCancellationRequested();
// The clean program is the drawing's own, rotated as placed: its material at the
@@ -80,7 +80,7 @@ internal static class SupportTabCapture
return null;
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,")
? FormattableString.Invariant($"is {support.Width:0.####} wide, in the width range tabbed by size,")
: support.IsHeldByTab
? FormattableString.Invariant($"is {support.Length:0.####} long, short enough for its tab alone to hold,")
: support.Margin == double.NegativeInfinity
@@ -101,14 +101,16 @@ internal static class SupportTabCapture
}
/// <summary>
/// One plan note when support tabs are on but the plate has no support bed and the settings
/// set no no-bed width range, so no part was tabbed. Advisory, like <see cref="Notes"/>.
/// One plan note when support tabs are on but go by size (no support bed, or by size chosen)
/// and the settings set no size range, so no part was tabbed. Advisory, like <see cref="Notes"/>.
/// </summary>
internal static IEnumerable<CuttingPlanFinding> Skipped(CuttingPlanSnapshot snapshot)
{
if (snapshot.SupportTabsSkipped)
yield return new CuttingPlanFinding(null, null, null, null, null,
"Support tabs are on, but the plate has no support bed and no width range is set for "
+ "plates without one, so no part was tabbed.");
snapshot.OwnedParameters?.TabBySize == true
? "Tabs by size are on, but no width range is set, so no part was tabbed."
: "Support tabs are on, but the plate has no support bed and no width range is set for "
+ "plates without one, so no part was tabbed.");
}
}
@@ -109,15 +109,15 @@ public class SupportGridPersistenceTests
[Fact]
public void NoBedTabRangeRoundTripsInCuttingParameters()
{
var parameters = new CuttingParameters { SupportTabs = true, NoBedTabMinWidth = 0.5, NoBedTabMaxWidth = 4 };
var parameters = new CuttingParameters { SupportTabs = true, TabSizeMinWidth = 0.5, TabSizeMaxWidth = 4 };
var restored = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
var snapshot = CuttingParametersSerializer.FromDto(CuttingParametersSerializer.ToDto(parameters));
Assert.Equal(0.5, restored.NoBedTabMinWidth);
Assert.Equal(4, restored.NoBedTabMaxWidth);
Assert.Equal(4, snapshot.NoBedTabMaxWidth);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").NoBedTabMaxWidth);
Assert.Equal(0.5, restored.TabSizeMinWidth);
Assert.Equal(4, restored.TabSizeMaxWidth);
Assert.Equal(4, snapshot.TabSizeMaxWidth);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").TabSizeMaxWidth);
}
[Fact]
+2
View File
@@ -33,6 +33,8 @@ public class CuttingParametersDto
public bool RoundLeadInAngles { get; set; }
public double LeadInAngleIncrement { get; set; }
public bool SupportTabs { get; set; }
public bool TabBySize { get; set; }
// The size range's JSON keys keep their names from when only plates with no support bed used it.
public double NoBedTabMinWidth { get; set; }
public double NoBedTabMaxWidth { get; set; }
public double TabHeldMaxLength { get; set; }
+6 -4
View File
@@ -71,8 +71,9 @@ public static class CuttingParametersSerializer
RoundLeadInAngles = parameters.RoundLeadInAngles,
LeadInAngleIncrement = parameters.LeadInAngleIncrement,
SupportTabs = parameters.SupportTabs,
NoBedTabMinWidth = parameters.NoBedTabMinWidth,
NoBedTabMaxWidth = parameters.NoBedTabMaxWidth,
TabBySize = parameters.TabBySize,
NoBedTabMinWidth = parameters.TabSizeMinWidth,
NoBedTabMaxWidth = parameters.TabSizeMaxWidth,
TabHeldMaxLength = parameters.TabHeldMaxLength,
Sequencing = ToSequenceDto(parameters.Sequencing),
Assignment = ToAssignmentDto(parameters.Assignment),
@@ -107,8 +108,9 @@ public static class CuttingParametersSerializer
RoundLeadInAngles = dto.RoundLeadInAngles,
LeadInAngleIncrement = dto.LeadInAngleIncrement,
SupportTabs = dto.SupportTabs,
NoBedTabMinWidth = dto.NoBedTabMinWidth,
NoBedTabMaxWidth = dto.NoBedTabMaxWidth,
TabBySize = dto.TabBySize,
TabSizeMinWidth = dto.NoBedTabMinWidth,
TabSizeMaxWidth = dto.NoBedTabMaxWidth,
TabHeldMaxLength = dto.TabHeldMaxLength,
Sequencing = FromSequenceDto(dto.Sequencing),
Assignment = FromAssignmentDto(dto.Assignment),
@@ -42,8 +42,8 @@ public class OwnedCuttingParameterTests
TabConfig = tab,
TabsEnabled = true,
SupportTabs = true,
NoBedTabMinWidth = 0.5,
NoBedTabMaxWidth = 4,
TabSizeMinWidth = 0.5,
TabSizeMaxWidth = 4,
TabHeldMaxLength = 2,
RoundLeadInAngles = true,
LeadInAngleIncrement = 30,
@@ -23,8 +23,8 @@ public class NoBedSupportTabTests
var parameters = ExplicitContourTests.Parameters();
parameters.SupportTabs = true;
parameters.TabConfig = new NormalTab { Size = TabSize };
parameters.NoBedTabMinWidth = min;
parameters.NoBedTabMaxWidth = max;
parameters.TabSizeMinWidth = min;
parameters.TabSizeMaxWidth = max;
return parameters;
}
@@ -54,10 +54,10 @@ public class NoBedSupportTabTests
[InlineData(1, 1, true)] // the bounds are inclusive
public void TheRangeTabsWidthsInsideIt(double min, double max, bool tabsAWidthOfOne)
{
var parameters = new CuttingParameters { NoBedTabMinWidth = min, NoBedTabMaxWidth = max };
var parameters = new CuttingParameters { TabSizeMinWidth = min, TabSizeMaxWidth = max };
Assert.Equal(tabsAWidthOfOne, parameters.TabsWithoutBedAt(1));
Assert.False(parameters.TabsWithoutBedAt(3));
Assert.Equal(tabsAWidthOfOne, parameters.TabsBySizeAt(1));
Assert.False(parameters.TabsBySizeAt(3));
}
[Fact]
@@ -161,7 +161,7 @@ public class NoBedSupportTabTests
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(untabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("without a support bed", finding.Message);
Assert.Contains("tabbed by size", finding.Message);
Assert.Contains("no tab", finding.Message);
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
+199
View File
@@ -0,0 +1,199 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.Support;
using OpenNest.Tests.CuttingPlanning;
namespace OpenNest.Tests.Support;
/// <summary>
/// Tabs by size (<see cref="CuttingParameters.TabBySize"/>): the support bed is ignored and every
/// part whose width is in the size range is tabbed, as on a plate with no bed; and the retired
/// "Tab every part" setting migrates to it in the user's settings only.
/// </summary>
public class TabBySizeTests
{
private const double TabSize = 0.2;
// Contacts every 2 along X and 4 along Y from (0, 2.25): a 10 x 1 strip at (2, 2) rests on
// one row of contacts, so the bed cannot hold it level.
private static SupportBed Bed() => new SupportGrid(2, 4, 0, 2.25, stabilityMargin: 0.1);
private static CuttingParameters Parameters(double min = 0.5, double max = 2, bool bySize = true)
{
var parameters = ExplicitContourTests.Parameters();
parameters.SupportTabs = true;
parameters.TabBySize = bySize;
parameters.TabConfig = new NormalTab { Size = TabSize };
parameters.TabSizeMinWidth = min;
parameters.TabSizeMaxWidth = max;
return parameters;
}
private static (Nest Nest, Plate Plate, Part Part) Strip(CuttingParameters parameters)
{
var nest = new Nest("by size");
var plate = new Plate(20, 20) { CuttingParameters = parameters, SupportBed = Bed() };
nest.Plates.Add(plate);
var part = new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 10, 1)), new Vector(2, 2));
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);
}
[Fact]
public void Analyze_BySizeIgnoresTheBed()
{
var (_, _, part) = Strip(Parameters());
var material = SupportMaterial.FromPart(part);
var byBed = SupportTabSettings.Analyze(Parameters(bySize: false), Bed(), 1, material);
Assert.True(byBed.HasBed);
Assert.False(byBed.IsSupported);
var bySize = SupportTabSettings.Analyze(Parameters(), Bed(), 1, material);
Assert.False(bySize.HasBed);
Assert.False(bySize.IsSupported);
// A size-tabbed part is held by any clear tab, so it is never a rapid obstacle.
Assert.True(bySize.TabCanSupport);
// Out of the range it is not tabbed, though the bed cannot hold it level.
Assert.Null(SupportTabSettings.Analyze(Parameters(2, 5), Bed(), 1, material));
}
[Fact]
public void CanTab_NeedsTheBedOrARange()
{
Assert.True(SupportTabSettings.CanTab(Parameters(0, 0, bySize: false), Bed()));
Assert.False(SupportTabSettings.CanTab(Parameters(0, 0), Bed()));
Assert.True(SupportTabSettings.CanTab(Parameters(), Bed()));
Assert.False(SupportTabSettings.CanTab(Parameters(0, 0, bySize: false), null));
Assert.True(SupportTabSettings.SkipsForNoRange(Parameters(0, 0), Bed()));
Assert.False(SupportTabSettings.SkipsForNoRange(Parameters(0, 0, bySize: false), Bed()));
}
[Fact]
public void PlanCutting_TabsAPartInTheRangeOnAPlateWithABed()
{
var (_, plate, _) = Strip(Parameters());
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
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 PlanCutting_LeavesAPartOutsideTheRangeUntabbedWithoutWarning()
{
var (_, plate, _) = Strip(Parameters(2, 5));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(2, 5)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(0, TabGap(Assert.Single(result.ProposedOrder)), 6);
Assert.Empty(result.Findings);
}
[Fact]
public void PlanCutting_NotesThatNothingWasTabbedWithNoRange()
{
var (_, plate, _) = Strip(Parameters(0, 0));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(0, 0)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(0, TabGap(Assert.Single(result.ProposedOrder)), 6);
var note = Assert.Single(result.Findings);
Assert.Null(note.Kind);
Assert.Contains("Tabs by size", note.Message);
}
[Fact]
public void ManualLeadIn_TabsAPartInTheRangeOnAPlateWithABed()
{
var parameters = Parameters();
var (_, plate, part) = Strip(parameters);
Assert.True(SupportTabSettings.ForManualLeadIn(parameters, plate, part).TabsEnabled);
var outOfRange = Parameters(2, 5);
Assert.Same(outOfRange, SupportTabSettings.ForManualLeadIn(outOfRange, plate, part));
}
[Fact]
public void PrePost_WarnsAboutAnUntabbedPartInTheRange_AndAcceptsAnyTab()
{
var (untabbed, _, bare) = Strip(Parameters());
bare.ApplySingleLeadIn(Parameters(), new Vector(0, 0.5), new Line(new Vector(0, 0), new Vector(0, 1)),
ContourType.External);
var (tabbed, _, held) = Strip(Parameters());
var withTabs = Parameters();
withTabs.TabsEnabled = true;
held.ApplySingleLeadIn(withTabs, new Vector(0, 0.5), new Line(new Vector(0, 0), new Vector(0, 1)),
ContourType.External);
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(untabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("tabbed by size", finding.Message);
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
}
[Fact]
public void MigrateTabEveryPart_TabsBySizeOverEveryWidth()
{
var parameters = new CuttingParameters { TabsEnabled = true, TabSizeMinWidth = 1, TabSizeMaxWidth = 2 };
Assert.True(SupportTabSettings.MigrateTabEveryPart(parameters));
Assert.False(parameters.TabsEnabled);
Assert.True(parameters.SupportTabs);
Assert.True(parameters.TabBySize);
Assert.Equal(0, parameters.TabSizeMinWidth);
Assert.Equal(SupportTabSettings.TabEveryPartMaxWidth, parameters.TabSizeMaxWidth);
Assert.True(parameters.TabsBySizeAt(500));
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, true)] // a part's copy that Plan Cutting tabbed
public void MigrateTabEveryPart_LeavesOtherSettingsAlone(bool tabsEnabled, bool supportTabs)
{
var parameters = new CuttingParameters { TabsEnabled = tabsEnabled, SupportTabs = supportTabs };
Assert.False(SupportTabSettings.MigrateTabEveryPart(parameters));
Assert.Equal(tabsEnabled, parameters.TabsEnabled);
Assert.Equal(supportTabs, parameters.SupportTabs);
Assert.False(parameters.TabBySize);
}
[Fact]
public void Serializer_RoundTripsTabBySize_AndNeverMigratesRecordedParameters()
{
var bySize = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(Parameters()));
Assert.True(bySize.TabBySize);
Assert.Equal(0.5, bySize.TabSizeMinWidth);
Assert.Equal(2, bySize.TabSizeMaxWidth);
// A plate's recorded parameters with tabs on every contour replay as planned.
var recorded = new CuttingParameters { TabsEnabled = true, TabConfig = new NormalTab { Size = TabSize } };
var read = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(recorded));
Assert.True(read.TabsEnabled);
Assert.False(read.SupportTabs);
Assert.False(read.TabBySize);
}
}
@@ -20,7 +20,7 @@ public class CuttingPanelTests
Kerf = 0.008,
ExternalLeadIn = new ArcLeadIn { Radius = 0.0625 },
ExternalLeadOut = new NoLeadOut(),
TabsEnabled = true,
SupportTabs = true,
TabConfig = new NormalTab { Size = 0.25 },
});
@@ -33,7 +33,59 @@ public class CuttingPanelTests
Assert.Equal(0.075, built.Thickness);
Assert.Equal(0.008, built.Kerf);
Assert.IsType<NoLeadOut>(built.ExternalLeadOut);
Assert.True(built.TabsEnabled);
Assert.True(built.SupportTabs);
}, TimeSpan.FromSeconds(30), "The cutting panel did not build its parameters.");
}
[Fact]
public void TabBySize_RoundTripsWithItsRange()
{
StaTestThread.Run(() =>
{
using var panel = new CuttingPanel();
panel.LoadFromParameters(new CuttingParameters
{
SupportTabs = true,
TabBySize = true,
TabSizeMinWidth = 0.5,
TabSizeMaxWidth = 3,
TabConfig = new NormalTab { Size = 0.03 },
});
var built = panel.BuildParameters();
Assert.True(built.SupportTabs);
Assert.True(built.TabBySize);
Assert.False(built.TabsEnabled);
Assert.Equal(0.5, built.TabSizeMinWidth);
Assert.Equal(3, built.TabSizeMaxWidth);
}, TimeSpan.FromSeconds(30), "The cutting panel did not build tabs by size.");
}
[Fact]
public void TabEveryPart_LoadsAsTabsBySizeOverEveryWidth()
{
StaTestThread.Run(() =>
{
using var panel = new CuttingPanel();
var saved = new CuttingParameters
{
TabsEnabled = true,
TabConfig = new NormalTab { Size = 0.03 },
TabSizeMaxWidth = 2,
};
panel.LoadFromParameters(saved);
var built = panel.BuildParameters();
Assert.False(built.TabsEnabled);
Assert.True(built.SupportTabs);
Assert.True(built.TabBySize);
Assert.Equal(0, built.TabSizeMinWidth);
Assert.Equal(OpenNest.Support.SupportTabSettings.TabEveryPartMaxWidth, built.TabSizeMaxWidth);
// The caller's settings are not changed.
Assert.True(saved.TabsEnabled);
Assert.False(saved.SupportTabs);
}, TimeSpan.FromSeconds(30), "The cutting panel did not migrate Tab every part.");
}
}
+61 -38
View File
@@ -3,6 +3,7 @@ using System.Drawing;
using System.Windows.Forms;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.IO;
using OpenNest.Support;
namespace OpenNest.Controls
{
@@ -24,11 +25,11 @@ namespace OpenNest.Controls
private readonly CheckBox chkTabsEnabled;
private readonly TableLayoutPanel tabOptions;
private readonly NumericUpDown nudTabWidth;
private readonly RadioButton rbTabAll;
private readonly RadioButton rbSupportTab;
private readonly RadioButton rbTabBySize;
private readonly TableLayoutPanel supportTabOptions;
private readonly NumericUpDown nudNoBedTabMin;
private readonly NumericUpDown nudNoBedTabMax;
private readonly NumericUpDown nudTabSizeMin;
private readonly NumericUpDown nudTabSizeMax;
private readonly NumericUpDown nudTabHeldMax;
private readonly NumericUpDown nudPierceClearance;
private readonly NumericUpDown nudPierceDiameter;
@@ -151,29 +152,19 @@ namespace OpenNest.Controls
nudTabWidth = CreateNumeric(0.25, 0.0625);
AddRow(tabOptions, "Tab size:", nudTabWidth, FieldIndent);
rbTabAll = new RadioButton
{
Text = "Tab every part",
AutoSize = true,
Checked = true,
Margin = new Padding(0, 4, 0, 0),
};
AddSpanningRow(tabOptions, rbTabAll);
// Plan Cutting tabs only the parts the plate's support grid cannot hold level.
rbSupportTab = new RadioButton
{
Text = "Only tab parts the support grid can't hold level",
AutoSize = true,
Margin = new Padding(0, 2, 0, 0),
Checked = true,
Margin = new Padding(0, 4, 0, 0),
};
toolTip.SetToolTip(rbSupportTab,
"Plan Cutting tabs each part that would tip once cut free, where it best keeps the part level.");
AddSpanningRow(tabOptions, rbSupportTab);
// 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.
// A part up to this long is held by its tab alone, wherever the tab goes. 0 turns it off.
supportTabOptions = new TableLayoutPanel
{
AutoSize = true,
@@ -192,22 +183,48 @@ namespace OpenNest.Controls
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))
AddSpanningRow(tabOptions, supportTabOptions);
// Tabs the parts whose narrow side is in the range below, whatever the support grid.
rbTabBySize = new RadioButton
{
Text = "Tab parts by size",
AutoSize = true,
Margin = new Padding(0, 2, 0, 0),
};
toolTip.SetToolTip(rbTabBySize,
"Ignores the support grid: tabs every part whose narrow side is in the range below.");
AddSpanningRow(tabOptions, rbTabBySize);
// The size range: the narrow side of each part's smallest bounding box, at any angle.
// Tabs by size use it, and so do support grid tabs on a plate with no support bed. A
// maximum of 0 tabs nothing.
var sizeRange = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Margin = new Padding(FieldIndent + 2, 0, 0, 0),
};
sizeRange.Controls.Add(
CreateHint("By size, or 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;
supportTabOptions.Controls.Add(
nudTabSizeMin = CreateNumeric(0, 0.25);
nudTabSizeMin.Width = 80;
nudTabSizeMax = CreateNumeric(0, 0.25);
nudTabSizeMax.Width = 80;
nudTabSizeMax.Maximum = (decimal)SupportTabSettings.TabEveryPartMaxWidth;
toolTip.SetToolTip(nudTabSizeMax,
"The narrow side of the part's smallest bounding box, at any angle, so a part's rotation does not matter.");
sizeRange.Controls.Add(
CreateInline(
nudNoBedTabMin,
nudTabSizeMin,
CreateHint("to", new Padding(4, 0, 4, 0)),
nudNoBedTabMax,
nudTabSizeMax,
CreateHint("(0 = none)", new Padding(6, 0, 0, 0))
)
);
AddSpanningRow(tabOptions, supportTabOptions);
AddSpanningRow(tabOptions, sizeRange);
layout.Controls.Add(tabOptions);
chkTabsEnabled.CheckedChanged += (s, e) =>
@@ -218,14 +235,14 @@ namespace OpenNest.Controls
};
// The two choices toggle together, so one handler sees every change.
rbTabAll.CheckedChanged += (s, e) =>
rbSupportTab.CheckedChanged += (s, e) =>
{
UpdateSupportTabOptions();
OnParametersChanged();
};
nudTabWidth.ValueChanged += (s, e) => OnParametersChanged();
nudNoBedTabMin.ValueChanged += (s, e) => OnParametersChanged();
nudNoBedTabMax.ValueChanged += (s, e) => OnParametersChanged();
nudTabSizeMin.ValueChanged += (s, e) => OnParametersChanged();
nudTabSizeMax.ValueChanged += (s, e) => OnParametersChanged();
nudTabHeldMax.ValueChanged += (s, e) => OnParametersChanged();
// Pierce
@@ -273,15 +290,18 @@ namespace OpenNest.Controls
parameters.ExternalLeadOut = externalLeads.LeadOut;
parameters.InternalLeadIn = internalLeads.LeadIn;
parameters.InternalLeadOut = internalLeads.LeadOut;
parameters.TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked;
// Tabs on every part are retired: Plan Cutting and Place Lead-in enable a tab on the
// copies they give the parts that need one.
parameters.TabsEnabled = false;
parameters.TabConfig = new NormalTab { Size = (double)nudTabWidth.Value };
parameters.PierceClearance = (double)nudPierceClearance.Value;
parameters.PierceDiameter = (double)nudPierceDiameter.Value;
parameters.RoundLeadInAngles = chkRoundLeadInAngles.Checked;
parameters.LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value;
parameters.SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked;
parameters.NoBedTabMinWidth = (double)nudNoBedTabMin.Value;
parameters.NoBedTabMaxWidth = (double)nudNoBedTabMax.Value;
parameters.SupportTabs = chkTabsEnabled.Checked;
parameters.TabBySize = rbTabBySize.Checked;
parameters.TabSizeMinWidth = (double)nudTabSizeMin.Value;
parameters.TabSizeMaxWidth = (double)nudTabSizeMax.Value;
parameters.TabHeldMaxLength = (double)nudTabHeldMax.Value;
return parameters;
}
@@ -290,19 +310,22 @@ namespace OpenNest.Controls
{
suppressEvents = true;
loaded = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(p));
// Settings saved with "Tab every part" show as tabs by size over every width.
SupportTabSettings.MigrateTabEveryPart(loaded);
p = loaded;
externalLeads.LeadIn = p.ExternalLeadIn;
externalLeads.LeadOut = p.ExternalLeadOut;
internalLeads.LeadIn = p.InternalLeadIn;
internalLeads.LeadOut = p.InternalLeadOut;
chkTabsEnabled.Checked = p.TabsEnabled || p.SupportTabs;
rbSupportTab.Checked = p.SupportTabs;
rbTabAll.Checked = !p.SupportTabs;
chkTabsEnabled.Checked = p.SupportTabs;
rbTabBySize.Checked = p.TabBySize;
rbSupportTab.Checked = !p.TabBySize;
if (p.TabConfig != null)
nudTabWidth.Value = Clamp(nudTabWidth, p.TabConfig.Size);
nudNoBedTabMin.Value = Clamp(nudNoBedTabMin, p.NoBedTabMinWidth);
nudNoBedTabMax.Value = Clamp(nudNoBedTabMax, p.NoBedTabMaxWidth);
nudTabSizeMin.Value = Clamp(nudTabSizeMin, p.TabSizeMinWidth);
nudTabSizeMax.Value = Clamp(nudTabSizeMax, p.TabSizeMaxWidth);
nudTabHeldMax.Value = Clamp(nudTabHeldMax, p.TabHeldMaxLength);
tabOptions.Enabled = chkTabsEnabled.Checked;
UpdateSupportTabOptions();
+5 -1
View File
@@ -232,7 +232,11 @@ namespace OpenNest.Forms
{
try
{
return CuttingParametersSerializer.Deserialize(json);
// Settings saved with the retired "Tab every part" read as tabs by size over
// every width. A plate's recorded parameters are never migrated.
var parameters = CuttingParametersSerializer.Deserialize(json);
Support.SupportTabSettings.MigrateTabEveryPart(parameters);
return parameters;
}
catch
{ /* fall through */
+4 -3
View File
@@ -41,9 +41,10 @@ var result = CuttingPlanService.Plan(snapshot, cancellationToken);
that still cannot hold its part level, or sits in an inside corner, is noted on the plan
without changing its status. A part no single tab can hold level may tip once cut, so its
tabbed outside is a rapid obstacle from then on, like an untabbed one. 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).
alone, so any tab clear of inside corners steadies it. Without a bed, or with
`CuttingParameters.TabBySize`, the parts whose width is in `TabSizeMinWidth`..`TabSizeMaxWidth`
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
operator settings or silently change lead styles to find a solution.
+46 -22
View File
@@ -82,11 +82,23 @@ Cutting inspects a proposal, the overlay shows the proposal's parts on the same
## Support tabs in Plan Cutting
Cutting Settings > Tabs > "Tab parts the support grid cannot hold level" sets
`CuttingParameters.SupportTabs`. With it and a plate support bed, `CuttingPlanService` capture
analyzes each regenerated part (`SupportTabCapture`). A part the bed cannot hold level gets a
tabbed perimeter (`PreparedContours.WithSupportTab`), using `TabConfig` for the tab size; all
other parts keep the plate settings, so supported parts are not tabbed.
Cutting Settings > Tabs > "Add tabs to hold parts in the sheet" sets
`CuttingParameters.SupportTabs`, and its two choices say which parts need a tab:
- "Only tab parts the support grid can't hold level": the plate's support bed decides. A plate
with no bed falls back to the size range ([below](#tabbing-by-size)).
- "Tab parts by size" (`CuttingParameters.TabBySize`): the bed is ignored and the size range
decides on every plate.
There is no setting that tabs every part: the former "Tab every part" choice is gone (see
[migration](#settings-saved-with-tab-every-part)). `CuttingParameters.TabsEnabled`, which tabs
every contour cut with those parameters, remains only on the copies Plan Cutting, Place Lead-in
and drawing pins give the parts they tab, and on parameters recorded that way.
With support tabs on, `CuttingPlanService` capture analyzes each regenerated part
(`SupportTabCapture`). A part that needs a tab gets a tabbed perimeter
(`PreparedContours.WithSupportTab`), using `TabConfig` for the tab size; all other parts keep the
plate settings, so parts that need no tab are not tabbed.
The emitter tabs a perimeter at its entry: the cut stops short of the entry by the tab size. A
round perimeter, one circle, is cut as an arc that stops the tab's chord short of its entry.
@@ -102,8 +114,8 @@ sets `CuttingParameters.TabHeldMaxLength`: on a plate with a support bed, a part
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; new settings
every tab clear of inside corners steadies it, and its best tab point is chosen as for a part
tabbed by size (below). A part the bed holds is not tabbed. 0 turns the rule off; new settings
start at 6, while settings saved without the field read as 0, so saved settings keep their
meaning. Some outlines have two minimum-area rectangles (a right isosceles triangle fits both
its legs' square and its hypotenuse's 2:1 box), and which one rounding finds depends on how the
@@ -113,14 +125,18 @@ long side, so a drawing's length and width are the same at every rotation.
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
## Tabbing by size
With no bed there are no contacts to judge a part by. Cutting Settings > Tabs > "No bed: tab
widths" sets `CuttingParameters.NoBedTabMinWidth` to `NoBedTabMaxWidth`: on a plate with no
support bed, support tabs then tab every part whose width (the short side of its outline's
minimum-area bounding rectangle, `SupportAnalyzer.Width`) is in that range, bounds included. A
maximum of 0 turns the rule off. `SupportTabSettings.Analyze` makes this choice for Plan Cutting,
hand-placed lead-ins and the pre-post review alike; a plate with a bed never uses the range.
Cutting Settings > Tabs > "By size, or with no support bed, tab parts whose narrow side is" sets
`CuttingParameters.TabSizeMinWidth` to `TabSizeMaxWidth` (stored under their earlier JSON names
`noBedTabMinWidth` and `noBedTabMaxWidth`, so saved settings and nests read unchanged). Support
tabs go by size under "Tab parts by size", and under the support grid choice on a plate with no
bed, where there are no contacts to judge a part by (`SupportTabSettings.UsesBed`). They then tab
every part whose width (the short side of its outline's minimum-area bounding rectangle,
`SupportAnalyzer.Width`) is in that range, bounds included. That rectangle may be turned at any
angle, so a drawing is tabbed or not however the nest rotated it. A maximum of 0 turns the rule
off. `SupportTabSettings.Analyze` makes this choice for Plan Cutting, hand-placed lead-ins and
the pre-post review alike; the support grid choice never uses the range on a plate with a bed.
`SupportAnalyzer.AnalyzeWithoutBed` gives such a part a `PartSupport` with `HasBed` false: no
contacts, never supported, and every tab clear of inside corners steadies it, so the perimeter's
@@ -128,8 +144,16 @@ automatic entries are every catalog point clear of inside corners plus the best
clear outline point nearest the line through the center of gravity across the long side. Which
of those wins is left to the usual ranking toward the next cut.
With support tabs on, no bed and no range, nothing is tabbed and the plan carries one advisory
note saying so; its status is unchanged.
When tabs go by size and no range is set, nothing is tabbed and the plan carries one advisory
note saying so (`SupportTabSettings.SkipsForNoRange`); its status is unchanged.
### Settings saved with "Tab every part"
The user's saved cutting settings with the former "Tab every part" (`TabsEnabled` on,
`SupportTabs` off) read as "Tab parts by size" from 0 to 9999 (`SupportTabSettings.MigrateTabEveryPart`),
which tabs the same parts. This happens only where the user's settings are read (the saved
settings and the cutting panel): a plate's recorded parameters and drawing pins keep
`TabsEnabled` and replay as planned.
If none of those entries has a lead-in that fits, the part is still tabbed. The search falls
back first to every catalog entry clear of inside corners, then to every entry
@@ -166,9 +190,9 @@ the plate.
## Lead-ins placed by hand
Plate > Place Lead-in cuts where it is clicked. Under "Tab parts the support grid cannot hold
level", `SupportTabSettings.ForManualLeadIn` enables tabs on a copy of the settings when the
plate's bed cannot hold the clicked part level, so the tab sits at the clicked entry. The
Plate > Place Lead-in cuts where it is clicked. With tabs on, `SupportTabSettings.ForManualLeadIn`
enables tabs on a copy of the settings when the clicked part needs one (the plate's bed cannot
hold it level or, by size, its width is in the range), so the tab sits at the clicked entry. The
overlay's orange diamond shows where it steadies the part; the pre-post review checks the
result. Plan Cutting is the only automatic lead-in assignment.
@@ -176,11 +200,11 @@ result. Plan Cutting is the only automatic lead-in assignment.
`PartSupportReview` runs in every pre-post verification and in Validate Nest. Each part whose
settings (its own, else the plate's) ask for support tabs and that needs one (the plate's bed
cannot hold it level or, with no bed, its width is in the no-bed range) is checked, however its
cannot hold it level or, by size, its width is in the size range) is checked, however its
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, 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
largest extent). A part with no such gap, or whose gap does not steady it (by size,
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: