From 6de23fb8316ddf213ec99eb0beb4cedfca119fb4 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 5 Nov 2026 07:20:55 -0500 Subject: [PATCH] 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 --- .../CuttingPlanning/OwnedCuttingParameters.cs | 5 +- .../CNC/CuttingPlanning/StateFingerprint.cs | 5 +- .../CNC/CuttingStrategy/CuttingParameters.cs | 51 +++-- .../Diagnostics/PartSupportReview.cs | 9 +- OpenNest.Core/Support/SupportAnalyzer.cs | 2 +- OpenNest.Core/Support/SupportTabSettings.cs | 69 ++++-- .../CuttingPlanning/CuttingPlanModels.cs | 5 +- .../CuttingPlanning/CuttingPlanService.cs | 2 +- .../CuttingPlanning/SupportTabCapture.cs | 20 +- .../SupportGridPersistenceTests.cs | 10 +- OpenNest.IO/CuttingParametersDto.cs | 2 + OpenNest.IO/CuttingParametersSerializer.cs | 10 +- .../OwnedCuttingParameterTests.cs | 4 +- .../Support/NoBedSupportTabTests.cs | 12 +- OpenNest.Tests/Support/TabBySizeTests.cs | 199 ++++++++++++++++++ .../Controls/CuttingPanelTests.cs | 56 ++++- OpenNest/Controls/CuttingPanel.cs | 99 +++++---- OpenNest/Forms/MachineSettings.cs | 6 +- docs/cutting-planner.md | 7 +- docs/support-tabs.md | 68 ++++-- 20 files changed, 503 insertions(+), 138 deletions(-) create mode 100644 OpenNest.Tests/Support/TabBySizeTests.cs diff --git a/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs b/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs index 32b83ff0..9999dcdc 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs @@ -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 { diff --git a/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs b/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs index 6a4e0fd0..213c1c8b 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs @@ -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); diff --git a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs index 81c38dc5..51e7f001 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs @@ -42,40 +42,55 @@ namespace OpenNest.CNC.CuttingStrategy public double LeadInAngleIncrement { get; set; } = 5.0; public Tab TabConfig { get; set; } + + /// + /// 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. + /// public bool TabsEnabled { get; set; } /// - /// 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 for the tab size. + /// Tabs the parts that need one (see docs/support-tabs.md): with + /// 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 for the tab size. /// public bool SupportTabs { get; set; } /// - /// With on a plate that has no support bed, the narrowest part - /// width that is tabbed instead (see ). + /// With , ignore the support bed and tab every part whose width is + /// in to + /// (). /// - public double NoBedTabMinWidth { get; set; } + public bool TabBySize { get; set; } /// - /// With 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 , or with + /// on a plate that has no support bed (see ). /// - public double NoBedTabMaxWidth { get; set; } + public double TabSizeMinWidth { get; set; } - /// Whether to is a usable range. - public bool HasNoBedTabRange() => - NoBedTabMaxWidth > 0 && double.IsFinite(NoBedTabMaxWidth) - && NoBedTabMinWidth >= 0 && NoBedTabMinWidth <= NoBedTabMaxWidth; + /// + /// The widest part width that is tabbed by size; 0 turns the rule off, so nothing is + /// tabbed by size. + /// + public double TabSizeMaxWidth { get; set; } + + /// Whether to is a usable range. + public bool HasTabSizeRange() => + TabSizeMaxWidth > 0 && double.IsFinite(TabSizeMaxWidth) + && TabSizeMinWidth >= 0 && TabSizeMinWidth <= TabSizeMaxWidth; /// /// Whether a part 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. /// - 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; /// /// With on a plate with a support bed, the longest part (the diff --git a/OpenNest.Core/Diagnostics/PartSupportReview.cs b/OpenNest.Core/Diagnostics/PartSupportReview.cs index db44ab60..836bea89 100644 --- a/OpenNest.Core/Diagnostics/PartSupportReview.cs +++ b/OpenNest.Core/Diagnostics/PartSupportReview.cs @@ -12,9 +12,9 @@ namespace OpenNest.Diagnostics; /// /// Pre-post review of part support (docs/support-tabs.md). Every part whose cutting parameters /// ask for support tabs and that needs one (: 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. /// 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.####}).") diff --git a/OpenNest.Core/Support/SupportAnalyzer.cs b/OpenNest.Core/Support/SupportAnalyzer.cs index 3071aac8..1a320cb2 100644 --- a/OpenNest.Core/Support/SupportAnalyzer.cs +++ b/OpenNest.Core/Support/SupportAnalyzer.cs @@ -67,7 +67,7 @@ namespace OpenNest.Support /// /// A part on a plate with no support bed, tabbed for its instead - /// (). With no contacts + /// (). 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. diff --git a/OpenNest.Core/Support/SupportTabSettings.cs b/OpenNest.Core/Support/SupportTabSettings.cs index 2c1589e2..46acfe8a 100644 --- a/OpenNest.Core/Support/SupportTabSettings.cs +++ b/OpenNest.Core/Support/SupportTabSettings.cs @@ -6,20 +6,41 @@ namespace OpenNest.Support { /// /// Which parts 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 (, + /// 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. /// public static class SupportTabSettings { + /// + /// 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. + /// + public const double TabEveryPartMaxWidth = 9999; + + /// + /// 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. + /// + public static bool UsesBed(CuttingParameters parameters, SupportBed bed) => + parameters?.SupportTabs == true && !parameters.TabBySize && bed != null; + + /// + /// Whether support tabs are on and can tab a part on a plate with : + /// by the bed, or by a usable size range. + /// + public static bool CanTab(CuttingParameters parameters, SupportBed bed) => + parameters?.SupportTabs == true && (UsesBed(parameters, bed) || parameters.HasTabSizeRange()); + /// /// 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 /// () when the bed cannot hold it level and its - /// length is within ; without one, a no-bed analysis + /// length is within ; by size (no bed, or + /// ), a no-bed analysis /// () of a part whose width is in - /// 's range. The part needs a tab when + /// 's range. The part needs a tab when /// the result is complete and not supported; an incomplete result could not be read. /// /// @@ -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; } /// - /// 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 + /// ) and the settings set no size range. /// - 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); /// /// 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; } + + /// + /// Moves the user's cutting settings off the retired "Tab every part" choice + /// ( on with support tabs off): they become tabs + /// by size from 0 to , which tabs the same parts. Returns + /// whether it changed . Only for the settings the user edits, + /// never a plate's recorded parameters or a drawing pin's, which replay as planned. + /// + 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; + } } } diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs index b55dbdc7..85c2d1fa 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs @@ -123,8 +123,9 @@ public sealed class CuttingPlanSnapshot internal IReadOnlyList Findings { get; } /// - /// 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. /// internal bool SupportTabsSkipped { get; init; } diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs index 3602acb2..19c3e011 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs @@ -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) diff --git a/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs b/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs index e040fc9d..63dc1f45 100644 --- a/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs +++ b/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs @@ -13,8 +13,8 @@ namespace OpenNest.Engine.CuttingPlanning; /// /// Capture-time support tabs for one regenerated part (docs/support-tabs.md): with /// on, a part the support bed -/// cannot hold level, or on a plate with no bed a part in the no-bed width range -/// (), has its perimeter tabbed where the tab steadies +/// cannot hold level, or by size (no bed, or ) +/// a part in the size range (), 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 } /// - /// 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 . + /// 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 . /// internal static IEnumerable 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."); } } diff --git a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs index 88cad8dd..e077778a 100644 --- a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs +++ b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs @@ -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] diff --git a/OpenNest.IO/CuttingParametersDto.cs b/OpenNest.IO/CuttingParametersDto.cs index 49584225..c1dd6ef7 100644 --- a/OpenNest.IO/CuttingParametersDto.cs +++ b/OpenNest.IO/CuttingParametersDto.cs @@ -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; } diff --git a/OpenNest.IO/CuttingParametersSerializer.cs b/OpenNest.IO/CuttingParametersSerializer.cs index be956805..c6787e4f 100644 --- a/OpenNest.IO/CuttingParametersSerializer.cs +++ b/OpenNest.IO/CuttingParametersSerializer.cs @@ -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), diff --git a/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs b/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs index 07650bbc..6cf28851 100644 --- a/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs +++ b/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs @@ -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, diff --git a/OpenNest.Tests/Support/NoBedSupportTabTests.cs b/OpenNest.Tests/Support/NoBedSupportTabTests.cs index bcb9c7b6..c8648430 100644 --- a/OpenNest.Tests/Support/NoBedSupportTabTests.cs +++ b/OpenNest.Tests/Support/NoBedSupportTabTests.cs @@ -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); diff --git a/OpenNest.Tests/Support/TabBySizeTests.cs b/OpenNest.Tests/Support/TabBySizeTests.cs new file mode 100644 index 00000000..76e36335 --- /dev/null +++ b/OpenNest.Tests/Support/TabBySizeTests.cs @@ -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; + +/// +/// Tabs by size (): 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. +/// +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); + } +} diff --git a/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs b/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs index 66e827e2..7a26ea5e 100644 --- a/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs +++ b/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs @@ -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(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."); + } } diff --git a/OpenNest/Controls/CuttingPanel.cs b/OpenNest/Controls/CuttingPanel.cs index cc81c16d..3a7c14af 100644 --- a/OpenNest/Controls/CuttingPanel.cs +++ b/OpenNest/Controls/CuttingPanel.cs @@ -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(); diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs index 3e058927..d07a2966 100644 --- a/OpenNest/Forms/MachineSettings.cs +++ b/OpenNest/Forms/MachineSettings.cs @@ -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 */ diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index 7c4ba597..a4c342f0 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -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. diff --git a/docs/support-tabs.md b/docs/support-tabs.md index cefe6e7e..7efb7946 100644 --- a/docs/support-tabs.md +++ b/docs/support-tabs.md @@ -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: