From 8c7dc41ce8e5a435364ab4e0e8821f02f8afdd18 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 20 Oct 2026 17:29:33 -0400 Subject: [PATCH] feat(planning): warn about tipping parts and retire legacy lead-in assignment Plan Cutting is now the only automatic lead-in assignment, and parts the support grid cannot hold level are reported wherever they lack a steadying tab. - Remove Assign Lead-ins (plate and nest), LeadInAssigner, the lead-in tool's Auto-Assign button and the Auto Tab size range; old settings with autoTab sizes still load - Manual Place Lead-in tabs the clicked entry when support tabs are on and the grid cannot hold the part (SupportTabSettings) - New PartSupport finding: Plan Cutting replay and best-effort plans warn when a drawing pin, a lead-in that does not fit or no single tab leaves a part unsteady; the pre-post review checks each placed program's outside tab gap. These warnings replace the advisory weak-tab notes; only the inside-corner tab note stays advisory, and never for a pin - Pinned perimeters keep their pin but carry the support analysis - Draw grid points as pixel-snapped squares without anti-aliasing - Post fixtures build lead-ins through a test-only TestLeadIns helper - Docs: support tabs, pre-post verification and planner notes Co-Authored-By: Claude Opus 5.5 --- .../CuttingPlanning/OwnedCuttingParameters.cs | 2 - .../CNC/CuttingPlanning/PreparedContours.cs | 32 +- .../CNC/CuttingPlanning/StateFingerprint.cs | 2 - .../CNC/CuttingStrategy/CuttingParameters.cs | 3 - .../Diagnostics/PartSupportReview.cs | 117 ++++++ .../Diagnostics/PostVerificationAnalyzer.cs | 2 + .../Diagnostics/PostVerificationReport.cs | 10 +- OpenNest.Core/Support/SupportAnalyzer.cs | 3 + OpenNest.Core/Support/SupportTabSettings.cs | 34 ++ .../CuttingPlanning/BestEffortCuttingPlan.cs | 2 + .../CuttingPlanning/CuttingPlanService.cs | 3 + .../CuttingPlanning/SupportTabCapture.cs | 71 +++- OpenNest.Engine/LeadInAssigner.cs | 108 ----- .../CuttingParametersSerializerTests.cs | 6 +- OpenNest.IO/CuttingParametersDto.cs | 2 - OpenNest.IO/CuttingParametersSerializer.cs | 4 - .../Breakoffs/BreakoffBuilderTests.cs | 2 +- .../CIFiberPostProcessorTests.cs | 2 +- .../CIFiberSampleRegressionTests.cs | 2 +- .../CuttingPlanning/DrawingPinReviewTests.cs | 47 +-- .../OwnedCuttingParameterTests.cs | 3 +- .../SupportTabPlanningTests.cs | 15 +- .../CuttingStrategy/LeadInAssignerTests.cs | 372 ------------------ .../IO/PartLeadInSerializationTests.cs | 7 +- OpenNest.Tests/Support/SupportReviewTests.cs | 124 ++++++ .../TabbedCutouts/TabbedCutoutBuilderTests.cs | 2 +- OpenNest.Tests/TestLeadIns.cs | 57 +++ .../CuttingParametersSerializerTests.cs | 18 +- OpenNest/Actions/ActionLeadIn.cs | 43 +- OpenNest/Controls/CuttingPanel.cs | 101 +---- OpenNest/Controls/SupportOverlay.cs | 32 +- OpenNest/Forms/CuttingInspectionPanel.cs | 2 + OpenNest/Forms/EditNestForm.cs | 70 ---- OpenNest/Forms/MainForm.Designer.cs | 22 +- OpenNest/Forms/MainForm.Nest.cs | 2 - OpenNest/Forms/MainForm.Plate.cs | 2 - docs/breakoffs.md | 2 +- docs/cutting-planner.md | 10 +- docs/geometry/lead-in-placement.md | 2 +- docs/post-verification.md | 3 + docs/support-tabs.md | 43 +- docs/tabbed-cutouts.md | 2 +- 42 files changed, 539 insertions(+), 849 deletions(-) create mode 100644 OpenNest.Core/Diagnostics/PartSupportReview.cs create mode 100644 OpenNest.Core/Support/SupportTabSettings.cs delete mode 100644 OpenNest.Engine/LeadInAssigner.cs delete mode 100644 OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs create mode 100644 OpenNest.Tests/Support/SupportReviewTests.cs create mode 100644 OpenNest.Tests/TestLeadIns.cs diff --git a/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs b/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs index a8a8c793..d36f1754 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/OwnedCuttingParameters.cs @@ -39,8 +39,6 @@ internal static class OwnedCuttingParameters ArcCircleLeadOut = CopyLeadOut(source.ArcCircleLeadOut), RoundLeadInAngles = source.RoundLeadInAngles, LeadInAngleIncrement = source.LeadInAngleIncrement, - AutoTabMinSize = Dimension(source.AutoTabMinSize), - AutoTabMaxSize = Dimension(source.AutoTabMaxSize), TabConfig = source.TabConfig == null ? source.TabsEnabled ? throw new ArgumentException("Enabled tabs require a configuration.") : null : CopyTab(source.TabConfig), diff --git a/OpenNest.Core/CNC/CuttingPlanning/PreparedContours.cs b/OpenNest.Core/CNC/CuttingPlanning/PreparedContours.cs index 44b66868..3d20ebac 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/PreparedContours.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/PreparedContours.cs @@ -92,7 +92,8 @@ public sealed class PreparedContours RejectedPins = rejected.AsReadOnly(); } - // A copy that tabs the perimeter for support; every pin keeps its entry, now owned here. + // A copy that tabs the perimeter for support; every pin keeps its entry, now owned here, and + // a pinned perimeter keeps its pin's settings: the analysis is only kept for review. private PreparedContours(PreparedContours source, PartSupport support, Vector location, Tab tab) : this(source.shapes, source.scribes, source.breakCuts, source.tabbedCutouts, source.parameters) { @@ -104,10 +105,13 @@ public sealed class PreparedContours contourParameters = source.contourParameters == null ? new CuttingParameters[Count] : (CuttingParameters[])source.contourParameters.Clone(); - var perimeter = OwnedCuttingParameters.Copy(source.ParametersFor(PerimeterOrdinal)); - perimeter.TabsEnabled = true; - perimeter.TabConfig = OwnedCuttingParameters.CopyTab(tab); - contourParameters[PerimeterOrdinal] = perimeter; + if (pinned[PerimeterOrdinal] == null) + { + var perimeter = OwnedCuttingParameters.Copy(source.ParametersFor(PerimeterOrdinal)); + perimeter.TabsEnabled = true; + perimeter.TabConfig = OwnedCuttingParameters.CopyTab(tab); + contourParameters[PerimeterOrdinal] = perimeter; + } RejectedPins = source.RejectedPins; this.support = support; supportOrigin = location; @@ -118,7 +122,8 @@ public sealed class PreparedContours /// on its support bed: analyzed this part's material placed at /// , and the perimeter's automatic entries keep to points it /// accepts () plus its best tab point. A pinned - /// perimeter is returned unchanged: the drawing wins. + /// perimeter keeps its pin's entry, leads and tab (the drawing wins); the copy only keeps + /// the analysis, so replay can report a pin that leaves the part unsteady. /// public PreparedContours WithSupportTab(PartSupport support, Vector location, Tab tab) { @@ -127,12 +132,21 @@ public sealed class PreparedContours PostVerificationGeometry.Validate(location); if (!support.IsComplete || support.BestTabPoint == null) throw new ArgumentException("Support tabs need a complete analysis with a tab point.", nameof(support)); - return IsPinned(PerimeterOrdinal) ? this : new PreparedContours(this, support, location, tab); + return new PreparedContours(this, support, location, tab); } - /// The support analysis that tabs this perimeter, or null when it is not tabbed for support. + /// + /// The analysis of a part its support bed cannot hold level (see ), + /// or null; places its local contours in its world frame. + /// public PartSupport Support => support; + /// The part's location: world = local + this, for . + public Vector SupportOrigin => supportOrigin; + + /// Whether the emitted perimeter is tabbed: by the plate settings, its pin or support. + internal bool PerimeterTabbed => ParametersFor(PerimeterOrdinal).TabsEnabled; + public int Count => shapes.Length; /// The pierce zone every lead-in pierce of these contours keeps; 0 when off. @@ -436,7 +450,7 @@ public sealed class PreparedContours /// Whether this preparation tabs its perimeter for support (see ): /// its automatic catalogue then keeps to steadying points unless asked for a fallback. /// - internal bool HasSupportTab => support != null; + internal bool HasSupportTab => support != null && !IsPinned(PerimeterOrdinal); /// /// The support margin with this perimeter's tab at (see diff --git a/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs b/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs index 9312b127..4fa9d58a 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/StateFingerprint.cs @@ -89,8 +89,6 @@ internal static class StateFingerprint Number(p.PierceDiameter); Flag(p.RoundLeadInAngles); Number(p.LeadInAngleIncrement); - Number(p.AutoTabMinSize); - Number(p.AutoTabMaxSize); TabSettings(p.TabConfig); Flag(p.TabsEnabled); Flag(p.SupportTabs); diff --git a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs index 9dbd1187..ba80dd17 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs @@ -40,9 +40,6 @@ namespace OpenNest.CNC.CuttingStrategy public bool RoundLeadInAngles { get; set; } public double LeadInAngleIncrement { get; set; } = 5.0; - public double AutoTabMinSize { get; set; } - public double AutoTabMaxSize { get; set; } - public Tab TabConfig { get; set; } public bool TabsEnabled { get; set; } diff --git a/OpenNest.Core/Diagnostics/PartSupportReview.cs b/OpenNest.Core/Diagnostics/PartSupportReview.cs new file mode 100644 index 00000000..9eb53d7e --- /dev/null +++ b/OpenNest.Core/Diagnostics/PartSupportReview.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using OpenNest.CNC; +using OpenNest.CNC.CuttingPlanning; +using OpenNest.Geometry; +using OpenNest.Support; + +namespace OpenNest.Diagnostics; + +/// +/// Pre-post review of part support (docs/support-tabs.md). On a plate with a support bed, every +/// part whose cutting parameters ask for support tabs and that the bed cannot hold level must be +/// cut with a tab that keeps it level, however its program was made (planned, placed by hand or +/// locked). 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 +{ + internal static void Add(Plate plate, int plateNumber, List findings, + CancellationToken token) + { + if (plate.SupportBed is not { } bed) + return; + for (var index = 0; index < plate.Parts.Count; index++) + { + token.ThrowIfCancellationRequested(); + var part = plate.Parts[index]; + if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff + || (part.CuttingParameters ?? plate.CuttingParameters)?.SupportTabs != true) + continue; + var partNumber = index + 1; + try + { + var support = SupportAnalyzer.Analyze(bed, plate.Quadrant, part); + if (!support.IsComplete) + { + findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null, + "Support check: " + support.Reason)); + continue; + } + if (support.IsSupported) + continue; + var tab = OutsideTab(ExecutionMotionReader.Read(part.Program, part.Location, null, token)); + if (tab is { } point && support.IsSteadiedByTabAt(point)) + continue; + findings.Add(new(PostVerificationKind.PartSupport, plateNumber, partNumber, null, + Message(support, tab)) + { Location = support.CenterOfGravity }); + } + catch (Exception exception) when (exception is ArgumentException or InvalidOperationException + or NotSupportedException or ArithmeticException) + { + findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null, + "Support check: " + exception.Message)); + } + } + } + + /// + /// The middle of the gap the outside cut leaves open, or null when it closes: the outside cut + /// is the chain of cutting motions with the largest extent. + /// + internal static Vector? OutsideTab(OwnedExecution execution) + { + Vector? start = null, end = null, bestStart = null, bestEnd = null; + var minX = double.PositiveInfinity; var minY = double.PositiveInfinity; + var maxX = double.NegativeInfinity; var maxY = double.NegativeInfinity; + var bestArea = -1.0; + foreach (var motion in execution.Motions.Append(null)) + { + if (motion is { Rapid: false, Layer: LayerType.Cut or LayerType.Display }) + { + start ??= motion.Start ?? motion.End; + end = motion.End; + foreach (var p in new[] { motion.Start ?? motion.End, motion.End, motion.Curve?.Midpoint ?? motion.End }) + { + minX = System.Math.Min(minX, p.X); minY = System.Math.Min(minY, p.Y); + maxX = System.Math.Max(maxX, p.X); maxY = System.Math.Max(maxY, p.Y); + } + continue; + } + // Anything else ends a chain: a rapid, a lead, a scribe or a break cut. + if (start != null && (maxX - minX) * (maxY - minY) > bestArea) + { + bestArea = (maxX - minX) * (maxY - minY); + bestStart = start; + bestEnd = end; + } + start = end = null; + minX = minY = double.PositiveInfinity; + maxX = maxY = double.NegativeInfinity; + } + if (bestStart is not { } first || bestEnd is not { } last + || first.DistanceTo(last) <= PostVerificationGeometry.Epsilon) + return null; + return (first + last) * 0.5; + } + + private static string Message(PartSupport support, Vector? tab) + { + var held = support.Margin == double.NegativeInfinity + ? "rests on no support contact" + : support.Margin < 0 + ? FormattableString.Invariant($"has its center of gravity {-support.Margin:0.####} outside its support contacts") + : FormattableString.Invariant($"has its center of gravity only {support.Margin:0.####} inside its support contacts"); + var tabText = tab is { } point + ? FormattableString.Invariant($"its tab at ({point.X:0.####}, {point.Y:0.####}) does not steady it") + : "it has no tab"; + var best = support.BestTabPoint is { } bestPoint && support.TabCanSupport + ? FormattableString.Invariant($" A tab near ({bestPoint.X:0.####}, {bestPoint.Y:0.####}) would.") + : " No single tab can steady it."; + return FormattableString.Invariant($"The part {held} (needs {support.RequiredMargin:0.####}) and {tabText}; ") + + "it may tip once cut free." + best; + } +} diff --git a/OpenNest.Core/Diagnostics/PostVerificationAnalyzer.cs b/OpenNest.Core/Diagnostics/PostVerificationAnalyzer.cs index 1880b54a..27548240 100644 --- a/OpenNest.Core/Diagnostics/PostVerificationAnalyzer.cs +++ b/OpenNest.Core/Diagnostics/PostVerificationAnalyzer.cs @@ -219,6 +219,8 @@ public static class PostVerificationAnalyzer issue.PartBId is { } other ? indices[other] : null, "Overlap check: " + issue.Message)); // Parts that do not overlap may still be closer than the plate's part spacing. PartSpacingReview.Add(plate, plateNumber, materialParts, indices, overlap, findings, cancellationToken); + // Parts the support bed cannot hold level need a tab that keeps them level. + PartSupportReview.Add(plate, plateNumber, findings, cancellationToken); } cancellationToken.ThrowIfCancellationRequested(); return new PostVerificationReport(findings); diff --git a/OpenNest.Core/Diagnostics/PostVerificationReport.cs b/OpenNest.Core/Diagnostics/PostVerificationReport.cs index 9556697d..b8cc1ac6 100644 --- a/OpenNest.Core/Diagnostics/PostVerificationReport.cs +++ b/OpenNest.Core/Diagnostics/PostVerificationReport.cs @@ -34,7 +34,13 @@ public enum PostVerificationKind /// Two parts are closer than the plate's part spacing without sharing material, judged as /// Auto Nest's layout check judges them. /// - PartSpacing + PartSpacing, + + /// + /// A part its plate's support bed cannot hold level has no tab that keeps it level, so it may + /// tip once cut free (see docs/support-tabs.md). + /// + PartSupport } /// Plate and part numbers are one-based; plate zero denotes a whole-post limitation. @@ -84,6 +90,8 @@ public sealed class PostVerificationReport Summary(PostVerificationKind.PierceZone, "Pierce zones"); if (Findings.Any(finding => finding.Kind == PostVerificationKind.PartSpacing)) Summary(PostVerificationKind.PartSpacing, "Part spacing"); + if (Findings.Any(finding => finding.Kind == PostVerificationKind.PartSupport)) + Summary(PostVerificationKind.PartSupport, "Part support"); foreach (var finding in Findings) { text.Append(finding.PlateNumber == 0 ? "Post processor" : $"Plate {finding.PlateNumber}"); diff --git a/OpenNest.Core/Support/SupportAnalyzer.cs b/OpenNest.Core/Support/SupportAnalyzer.cs index 7ec44af0..5670116d 100644 --- a/OpenNest.Core/Support/SupportAnalyzer.cs +++ b/OpenNest.Core/Support/SupportAnalyzer.cs @@ -365,6 +365,9 @@ namespace OpenNest.Support public double MarginWith(Vector tab) => IsComplete ? SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab) : double.NegativeInfinity; + /// True when a tab at holds the part level by the required margin. + public bool IsSteadiedByTabAt(Vector tab) => IsComplete && Meets(MarginWith(tab)); + /// /// Whether a tab at is good enough: it keeps clear of inside corners, /// and it holds the part level by the required margin or, where no single tab can, it is diff --git a/OpenNest.Core/Support/SupportTabSettings.cs b/OpenNest.Core/Support/SupportTabSettings.cs new file mode 100644 index 00000000..2de4ff57 --- /dev/null +++ b/OpenNest.Core/Support/SupportTabSettings.cs @@ -0,0 +1,34 @@ +using System; +using OpenNest.CNC.CuttingPlanning; +using OpenNest.CNC.CuttingStrategy; + +namespace OpenNest.Support +{ + /// + /// Cutting settings for a lead-in placed by hand under + /// : the tool cuts where it is clicked, so the + /// part is tabbed there when its plate's support bed cannot hold it level. Plan Cutting + /// chooses the tab point itself (docs/support-tabs.md). + /// + public static class SupportTabSettings + { + /// + /// itself, or a copy with tabs enabled when support tabs are + /// on, the plate has a support bed, a tab size is set and the bed cannot hold + /// level. The caller's settings are never changed. + /// + public static CuttingParameters ForManualLeadIn(CuttingParameters parameters, Plate plate, Part part) + { + ArgumentNullException.ThrowIfNull(parameters); + if (!parameters.SupportTabs || parameters.TabsEnabled || parameters.TabConfig == null + || plate?.SupportBed == null || part == null) + return parameters; + var support = SupportAnalyzer.Analyze(plate.SupportBed, plate.Quadrant, part); + if (!support.IsComplete || support.IsSupported) + return parameters; + var tabbed = OwnedCuttingParameters.Copy(parameters); + tabbed.TabsEnabled = true; + return tabbed; + } + } +} diff --git a/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs b/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs index acdd6912..b8b4403e 100644 --- a/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs +++ b/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs @@ -75,6 +75,8 @@ internal static class BestEffortCuttingPlan f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null, f.Kind, f.Message) { Witness = f })); + if (SupportTabCapture.Review(source, proposal.ContourChoices) is { } unsteady) + findings.Add(unsteady); findings.AddRange(SupportTabCapture.Notes(proposal)); distance += execution.RapidDistanceFrom(position); if (!double.IsFinite(distance)) diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs index 0a50c5e2..cfaba8e6 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs @@ -397,6 +397,9 @@ public static class CuttingPlanService findings: [JointCuttingPlanSearch.Finding(source, null, "Selected program does not preserve complete directed contours, certified tabs or selected entry/style geometry.")], expansions: expansions); + // A part the support bed cannot hold level must be tabbed where the tab steadies it. + if (SupportTabCapture.Review(source, proposal.ContourChoices) is { } unsteady) + warnings.Add(unsteady); distance += execution.RapidDistanceFrom(position); position = execution.DeparturePoint; } diff --git a/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs b/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs index 8b525980..a84deb76 100644 --- a/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs +++ b/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; +using OpenNest.Diagnostics; using OpenNest.Geometry; using OpenNest.Support; @@ -12,9 +13,10 @@ namespace OpenNest.Engine.CuttingPlanning; /// /// Capture-time support tabs for one regenerated part (docs/support-tabs.md): with a support /// bed and on, a part the bed -/// cannot hold level has its perimeter tabbed where the tab steadies it. A supported part, a -/// pinned perimeter (the drawing wins), a missing tab configuration or material the analyzer -/// cannot read leave the preparation as it was. +/// cannot hold level 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 +/// preparation as it was. /// internal static class SupportTabCapture { @@ -23,7 +25,7 @@ internal static class SupportTabCapture { var parameters = request.ConfirmedParameters; if (prepared == null || request.SupportBed == null || parameters?.SupportTabs != true - || parameters.TabConfig == null || prepared.IsPinned(prepared.PerimeterOrdinal)) + || parameters.TabConfig == null) return prepared; token.ThrowIfCancellationRequested(); // The clean program is the drawing's own, rotated as placed: its material at the @@ -38,29 +40,56 @@ internal static class SupportTabCapture } /// - /// Notes for a planned support-tabbed part whose tab, at its perimeter entry, cannot hold it - /// level or sits in an inside corner. Advisory: they never refuse or downgrade a plan. + /// A note for a planned support-tabbed part whose tab, at its perimeter entry, sits in an + /// inside corner because no lead-in fits clear of one. Advisory: it never refuses or + /// downgrades a plan. A tab that cannot steady its part is 's warning. /// internal static IEnumerable Notes(FixedProgramPlacement planned) { var prepared = planned.Prepared; - if (prepared?.Support is not { } support - || planned.ContourChoices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal) is not { } entry) + if (prepared?.Support == null || prepared.IsPinned(prepared.PerimeterOrdinal) + || planned.ContourChoices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal) is not { } entry + || prepared.IsClearOfInsideCorners(entry)) yield break; var tab = entry.Point + planned.Location; var at = FormattableString.Invariant($"({tab.X:0.####}, {tab.Y:0.####})"); - if (!support.TabCanSupport) - yield return JointCuttingPlanSearch.Finding(planned, null, - $"The support grid cannot hold it level, and no single tab can: its tab at {at} steadies it " - + "as far as one tab can, but it may still tip. Add tabs by hand or move the part, then replan."); - else if (!prepared.HoldsLevelAt(entry)) - yield return JointCuttingPlanSearch.Finding(planned, null, - $"The support grid cannot hold it level, and no lead-in fits where a tab would: its tab at {at} " - + "is the steadiest that fits, but it may still tip. Shorten the lead-in in Cutting Settings or " - + "space the parts farther apart, then replan."); - if (!prepared.IsClearOfInsideCorners(entry)) - yield return JointCuttingPlanSearch.Finding(planned, null, - $"Its support tab at {at} is in an inside corner, because no lead-in fits clear of one; " - + "its nub will be hard to grind flush."); + yield return JointCuttingPlanSearch.Finding(planned, null, + $"Its support tab at {at} is in an inside corner, because no lead-in fits clear of one; " + + "its nub will be hard to grind flush."); + } + + /// + /// A warning when a regenerated part its bed + /// cannot hold level is cut without a tab that keeps it level: its pin has no tab or a tab + /// elsewhere, no steadying entry had a lead-in that fits, or no single tab can steady it. + /// Null otherwise. Like other warnings it makes the proposal UNVERIFIED. + /// + internal static CuttingPlanFinding Review(FixedProgramPlacement source, IReadOnlyList choices) + { + var prepared = source.Prepared; + if (prepared?.Support is not { } support || choices == null) + return null; + var perimeter = choices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal); + if (perimeter == null) + return null; + var tab = perimeter.Point + prepared.SupportOrigin; + if (prepared.PerimeterTabbed && support.IsSteadiedByTabAt(tab)) + return null; + var part = source.SourceOrdinal + 1; + var held = support.Margin == double.NegativeInfinity + ? "rests on no support contact" + : support.Margin < 0 + ? FormattableString.Invariant($"has its center of gravity {-support.Margin:0.####} outside its support contacts") + : FormattableString.Invariant($"has its center of gravity only {support.Margin:0.####} inside its support contacts"); + var why = !prepared.PerimeterTabbed + ? $"drawing pin {prepared.PinNumberOf(prepared.PerimeterOrdinal)} cuts its outside without a tab" + : prepared.IsPinned(prepared.PerimeterOrdinal) + ? $"drawing pin {prepared.PinNumberOf(prepared.PerimeterOrdinal)} puts its tab where it does not steady the part" + : support.TabCanSupport + ? "no lead-in fits at a point where its tab would steady the part" + : "no single tab can steady it"; + return new CuttingPlanFinding(source.SourceOrdinal, source.SourcePart, null, null, + PostVerificationKind.PartSupport, FormattableString.Invariant( + $"Part {part} {held} (needs {support.RequiredMargin:0.####}) and {why}; it may tip once cut free.")); } } diff --git a/OpenNest.Engine/LeadInAssigner.cs b/OpenNest.Engine/LeadInAssigner.cs deleted file mode 100644 index df4bf76a..00000000 --- a/OpenNest.Engine/LeadInAssigner.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; - -namespace OpenNest.Engine -{ - public class LeadInAssigner - { - public IPartSequencer Sequencer { get; set; } - - /// - /// Assigns the plate's lead-ins to every unlocked part, except parts whose drawing pins - /// are active: those keep their programs, since only Plan Cutting cuts drawing pins, and - /// this never overwrites a pin with the plate's settings. Returns those parts. - /// - public IReadOnlyList Assign(Plate plate) - { - var parameters = plate.CuttingParameters; - if (parameters == null) - return []; - - var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate); - var exitPoint = PlateHelper.GetExitPoint(plate); - - // Pass 1: assign lead-ins to establish pierce points - var piercePoints = AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: null); - - // Pass 2: re-assign with knowledge of next part's start point - AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints); - - return plate.Parts.Where(KeepsDrawingLeadIns).ToList(); - } - - /// - /// The operator's notice after Assign Lead-ins kept parts with drawing pins, or null when it - /// kept none. Part numbers are the editor's (the plate's current order). - /// - public static string KeptMessage(IReadOnlyList<(int PlateNumber, IReadOnlyList PartNumbers)> kept) - { - var count = kept.Sum(k => k.PartNumbers.Count); - if (count == 0) - return null; - var where = string.Join("; ", kept.Where(k => k.PartNumbers.Count != 0).Select(k => - $"plate {k.PlateNumber}: part{(k.PartNumbers.Count == 1 ? "" : "s")} {string.Join(", ", k.PartNumbers)}")); - return $"{count} part{(count == 1 ? "" : "s")} kept {(count == 1 ? "its" : "their")} drawing lead-ins ({where}); " - + "use Plan Cutting to apply their drawing pins."; - } - - // Locked parts are skipped as before and keep their locked lead-ins whatever their pins. - private static bool KeepsDrawingLeadIns(Part part) => !part.LeadInsLocked && part.HasActiveDrawingPins; - - private Vector[] AssignPass( - List sequenced, - CuttingParameters parameters, - Vector exitPoint, - Vector[] nextPiercePoints - ) - { - var piercePoints = new Vector[sequenced.Count]; - var currentPoint = exitPoint; - - for (var i = 0; i < sequenced.Count; i++) - { - var part = sequenced[i].Part; - - if (part.LeadInsLocked || KeepsDrawingLeadIns(part)) - { - piercePoints[i] = GetPiercePoint(part); - currentPoint = part.Location; - continue; - } - - if (part.HasManualLeadIns) - part.RemoveLeadIns(); - - var localApproach = currentPoint - part.Location; - - if (nextPiercePoints != null && i + 1 < sequenced.Count) - { - var nextStart = nextPiercePoints[i + 1] - part.Location; - part.ApplyLeadIns(parameters, localApproach, nextStart); - } - else - { - part.ApplyLeadIns(parameters, localApproach); - } - - piercePoints[i] = GetPiercePoint(part); - currentPoint = part.Location; - } - - return piercePoints; - } - - private static Vector GetPiercePoint(Part part) - { - foreach (var code in part.Program.Codes) - { - if (code is CNC.Motion motion) - return motion.EndPoint + part.Location; - } - - return part.Location; - } - } -} diff --git a/OpenNest.IO.Tests/CuttingParametersSerializerTests.cs b/OpenNest.IO.Tests/CuttingParametersSerializerTests.cs index fc1c83d0..dced3c4e 100644 --- a/OpenNest.IO.Tests/CuttingParametersSerializerTests.cs +++ b/OpenNest.IO.Tests/CuttingParametersSerializerTests.cs @@ -128,8 +128,7 @@ public class CuttingParametersSerializerTests Assert.Equal(0.0625, restored.PierceClearance); Assert.False(restored.RoundLeadInAngles); Assert.Equal(5, restored.LeadInAngleIncrement); - Assert.Equal(0, restored.AutoTabMinSize); - Assert.Equal(0, restored.AutoTabMaxSize); + Assert.False(restored.SupportTabs); AssertEquivalent(new AssignmentParameters(), restored.Assignment); AssertEquivalent(new SequenceParameters(), restored.Sequencing); } @@ -304,8 +303,7 @@ public class CuttingParametersSerializerTests PierceDiameter = 0.41, RoundLeadInAngles = true, LeadInAngleIncrement = 13, - AutoTabMinSize = 0.62, - AutoTabMaxSize = 4.7, + SupportTabs = true, TabConfig = CreateTab("Breaker"), TabsEnabled = true, Assignment = new AssignmentParameters diff --git a/OpenNest.IO/CuttingParametersDto.cs b/OpenNest.IO/CuttingParametersDto.cs index 47302376..d25a1185 100644 --- a/OpenNest.IO/CuttingParametersDto.cs +++ b/OpenNest.IO/CuttingParametersDto.cs @@ -31,8 +31,6 @@ public class CuttingParametersDto public double PierceDiameter { get; set; } public bool RoundLeadInAngles { get; set; } public double LeadInAngleIncrement { get; set; } - public double AutoTabMinSize { get; set; } - public double AutoTabMaxSize { get; set; } public bool SupportTabs { get; set; } public SequenceDto Sequencing { get; set; } = new(); public AssignmentDto Assignment { get; set; } = new(); diff --git a/OpenNest.IO/CuttingParametersSerializer.cs b/OpenNest.IO/CuttingParametersSerializer.cs index 369b4a8a..67b67397 100644 --- a/OpenNest.IO/CuttingParametersSerializer.cs +++ b/OpenNest.IO/CuttingParametersSerializer.cs @@ -72,8 +72,6 @@ public static class CuttingParametersSerializer PierceDiameter = parameters.PierceDiameter, RoundLeadInAngles = parameters.RoundLeadInAngles, LeadInAngleIncrement = parameters.LeadInAngleIncrement, - AutoTabMinSize = parameters.AutoTabMinSize, - AutoTabMaxSize = parameters.AutoTabMaxSize, SupportTabs = parameters.SupportTabs, Sequencing = ToSequenceDto(parameters.Sequencing), Assignment = ToAssignmentDto(parameters.Assignment), @@ -109,8 +107,6 @@ public static class CuttingParametersSerializer PierceDiameter = dto.PierceDiameter, RoundLeadInAngles = dto.RoundLeadInAngles, LeadInAngleIncrement = dto.LeadInAngleIncrement, - AutoTabMinSize = dto.AutoTabMinSize, - AutoTabMaxSize = dto.AutoTabMaxSize, SupportTabs = dto.SupportTabs, Sequencing = FromSequenceDto(dto.Sequencing), Assignment = FromAssignmentDto(dto.Assignment), diff --git a/OpenNest.Tests/Breakoffs/BreakoffBuilderTests.cs b/OpenNest.Tests/Breakoffs/BreakoffBuilderTests.cs index 69604eea..86f17b88 100644 --- a/OpenNest.Tests/Breakoffs/BreakoffBuilderTests.cs +++ b/OpenNest.Tests/Breakoffs/BreakoffBuilderTests.cs @@ -212,7 +212,7 @@ public class BreakoffBuilderTests } [Fact] - public void AssignLeadIns_LeadsBreakCutsInFromTheScrap() + public void ApplyLeadIns_LeadsBreakCutsInFromTheScrap() { var (entities, join) = Chord(); var drawing = Build(entities, On(join)); diff --git a/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs b/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs index 8c5284f2..31b54762 100644 --- a/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs +++ b/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs @@ -186,7 +186,7 @@ public class CIFiberPostProcessorTests ExternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, }; - new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate); + TestLeadIns.Apply(plate); var nest = new Nest("kerf") { Thickness = 0.06, Material = new Material("Mild Steel") }; nest.Plates.Add(plate); diff --git a/OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs b/OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs index 06dcda43..9663e85d 100644 --- a/OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs +++ b/OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs @@ -65,7 +65,7 @@ public class CIFiberSampleRegressionTests PierceClearance = 0.02, }; - new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate); + TestLeadIns.Apply(plate); return nest; } diff --git a/OpenNest.Tests/CuttingPlanning/DrawingPinReviewTests.cs b/OpenNest.Tests/CuttingPlanning/DrawingPinReviewTests.cs index 854ca1f4..d6a66973 100644 --- a/OpenNest.Tests/CuttingPlanning/DrawingPinReviewTests.cs +++ b/OpenNest.Tests/CuttingPlanning/DrawingPinReviewTests.cs @@ -10,54 +10,11 @@ using OpenNest.Geometry; namespace OpenNest.Tests.CuttingPlanning; /// -/// Legacy Assign Lead-ins never overwrites drawing pins, and the pre-post review reports a part -/// whose active pins its program does not apply, plus fixed (locked or pinned) lead violations. +/// The pre-post review reports a part whose active pins its program does not apply, plus fixed +/// (locked or pinned) lead violations. /// public class DrawingPinReviewTests { - [Fact] - public void AssignLeadIns_KeepsPinnedPartsAndReturnsThem() - { - var plate = new Plate(60, 120); - var pinned = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(10, 10)); - var plain = new Part(new Drawing("plain", ExplicitContourTests.Square(false)), new Vector(30, 10)); - var off = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(10, 30)) - { DrawingPinsOff = true }; - var locked = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(30, 30)) - { LeadInsLocked = true }; - foreach (var part in new[] { pinned, plain, off, locked }) - plate.Parts.Add(part); - var pinnedProgram = pinned.Program; - var lockedProgram = locked.Program; - plate.CuttingParameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } }; - - var kept = new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate); - - Assert.Equal([pinned], kept); - Assert.Same(pinnedProgram, pinned.Program); - Assert.False(pinned.HasManualLeadIns); - Assert.Same(lockedProgram, locked.Program); - Assert.True(plain.HasManualLeadIns); - Assert.True(off.HasManualLeadIns); - Assert.Equal("1 part kept its drawing lead-ins (plate 2: part 1); use Plan Cutting to apply their drawing pins.", - LeadInAssigner.KeptMessage([(2, [1])])); - Assert.Equal("3 parts kept their drawing lead-ins (plate 1: parts 1, 4; plate 3: part 2); use Plan Cutting " - + "to apply their drawing pins.", LeadInAssigner.KeptMessage([(1, [1, 4]), (2, []), (3, [2])])); - Assert.Null(LeadInAssigner.KeptMessage([(1, [])])); - } - - [Fact] - public void AssignLeadIns_WithoutPins_ReturnsNothingAndAssignsAsBefore() - { - var plate = new Plate(60, 120); - plate.Parts.Add(new Part(new Drawing("plain", ExplicitContourTests.Square(false)), new Vector(10, 10))); - plate.CuttingParameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } }; - - Assert.Empty(new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate)); - Assert.True(plate.Parts[0].HasManualLeadIns); - Assert.Empty(new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(new Plate(10, 10))); - } - [Fact] public void PrePost_PinsAppliedByPlanCutting_AreNotReported() { diff --git a/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs b/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs index bcdfe432..0260e5af 100644 --- a/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs +++ b/OpenNest.Tests/CuttingPlanning/OwnedCuttingParameterTests.cs @@ -42,8 +42,7 @@ public class OwnedCuttingParameterTests ArcCircleLeadOut = leadOut, TabConfig = tab, TabsEnabled = true, - AutoTabMinSize = 0.1, - AutoTabMaxSize = 0.5, + SupportTabs = true, RoundLeadInAngles = true, LeadInAngleIncrement = 30, Sequencing = new SequenceParameters diff --git a/OpenNest.Tests/CuttingPlanning/SupportTabPlanningTests.cs b/OpenNest.Tests/CuttingPlanning/SupportTabPlanningTests.cs index b9d44d3f..aa02ae61 100644 --- a/OpenNest.Tests/CuttingPlanning/SupportTabPlanningTests.cs +++ b/OpenNest.Tests/CuttingPlanning/SupportTabPlanningTests.cs @@ -1,6 +1,7 @@ using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; using OpenNest.CNC.CuttingStrategy; +using OpenNest.Diagnostics; using OpenNest.Engine.CuttingPlanning; using OpenNest.Geometry; using OpenNest.Support; @@ -104,7 +105,7 @@ public class SupportTabPlanningTests var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(7, 0), confirmedParameters: Parameters())); - Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message))); + Assert.True(result.Status == CuttingPlanStatus.BestEffort, string.Join("\n", result.Findings.Select(f => f.Message))); var proposal = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, strip)); var (entry, cutEnd) = EntryAndCutEnd(proposal); Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6); @@ -115,7 +116,8 @@ public class SupportTabPlanningTests Assert.True(support.MarginWith(entry) > support.MarginWith(new Vector(12, 2)), $"entry {entry}"); Assert.False(support.HoldsLevelWithTabAt(entry)); Assert.Contains(result.Findings, f => ReferenceEquals(f.SourcePart, strip) - && f.Message.Contains("no lead-in fits where a tab would")); + && f.Kind == PostVerificationKind.PartSupport + && f.Message.Contains("no lead-in fits at a point where its tab would steady the part")); } [Fact] @@ -130,12 +132,13 @@ public class SupportTabPlanningTests var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(0, 0), confirmedParameters: Parameters())); - Assert.Equal(CuttingPlanStatus.Ready, result.Status); + Assert.Equal(CuttingPlanStatus.BestEffort, result.Status); var (entry, cutEnd) = EntryAndCutEnd(Assert.Single(result.ProposedOrder)); Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6); - var note = Assert.Single(result.Findings); - Assert.Same(strip, note.SourcePart); - Assert.Contains("no single tab can", note.Message); + var warning = Assert.Single(result.Findings); + Assert.Same(strip, warning.SourcePart); + Assert.Equal(PostVerificationKind.PartSupport, warning.Kind); + Assert.Contains("no single tab can steady it", warning.Message); } [Fact] diff --git a/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs b/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs deleted file mode 100644 index 8794118b..00000000 --- a/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs +++ /dev/null @@ -1,372 +0,0 @@ -using OpenNest.CNC; -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Engine; -using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; - -namespace OpenNest.Tests.CuttingStrategy; - -public class LeadInAssignerTests -{ - private static Part MakeSquarePartAt(double x, double y) - { - var pgm = new Program(); - pgm.Codes.Add(new RapidMove(new Vector(0, 0))); - pgm.Codes.Add(new LinearMove(new Vector(0, 10))); - pgm.Codes.Add(new LinearMove(new Vector(10, 10))); - pgm.Codes.Add(new LinearMove(new Vector(10, 0))); - pgm.Codes.Add(new LinearMove(new Vector(0, 0))); - var drawing = new Drawing("test", pgm); - return new Part(drawing, new Vector(x, y)); - } - - [Fact] - public void Assign_SetsHasManualLeadInsOnAllParts() - { - var plate = new Plate(60, 120); - plate.Parts.Add(MakeSquarePartAt(10, 10)); - plate.Parts.Add(MakeSquarePartAt(30, 30)); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - Assert.All(plate.Parts, p => Assert.True(p.HasManualLeadIns)); - } - - [Fact] - public void Assign_SkipsLockedParts() - { - var plate = new Plate(60, 120); - var lockedPart = MakeSquarePartAt(10, 10); - lockedPart.LeadInsLocked = true; - lockedPart.HasManualLeadIns = true; - var originalProgram = lockedPart.Program; - - plate.Parts.Add(lockedPart); - plate.Parts.Add(MakeSquarePartAt(30, 30)); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - Assert.Same(originalProgram, lockedPart.Program); - } - - [Fact] - public void Assign_RemovesExistingLeadInsBeforeReapply() - { - var plate = new Plate(60, 120); - plate.Parts.Add(MakeSquarePartAt(10, 10)); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - var countAfterFirst = plate.Parts[0].Program.Codes.Count; - - assigner.Assign(plate); - var countAfterSecond = plate.Parts[0].Program.Codes.Count; - - Assert.Equal(countAfterFirst, countAfterSecond); - } - - [Fact] - public void Assign_PartsContainLeadinLayerCodes() - { - var plate = new Plate(60, 120); - plate.Parts.Add(MakeSquarePartAt(10, 10)); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - var hasLeadin = plate - .Parts[0] - .Program.Codes.OfType() - .Any(m => m.Layer == LayerType.Leadin); - Assert.True(hasLeadin); - } - - [Fact] - public void Assign_PreservesRotationOnRotatedParts() - { - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 2; // 90 degrees - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - Assert.Equal(rotation, part.Rotation, 6); - } - - [Fact] - public void Assign_ThenRemove_PreservesRotation() - { - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 4; // 45 degrees - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(15, 15); - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - Assert.Equal(rotation, part.Rotation, 6); - - part.RemoveLeadIns(); - Assert.Equal(rotation, part.Rotation, 6); - } - - [Fact] - public void Assign_PreservesLocationOnRotatedParts() - { - var drawing = MakeSquareDrawing(); - var part = Part.CreateAtOrigin(drawing, System.Math.PI / 2); - part.Offset(20, 30); - var location = part.Location; - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - Assert.Equal(location.X, part.Location.X, 6); - Assert.Equal(location.Y, part.Location.Y, 6); - } - - [Fact] - public void Assign_ThenRemove_RestoresBoundingBox() - { - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 2; - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - var originalBbox = part.BoundingBox; - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - part.RemoveLeadIns(); - - Assert.Equal(originalBbox.X, part.BoundingBox.X, 4); - Assert.Equal(originalBbox.Y, part.BoundingBox.Y, 4); - Assert.Equal(originalBbox.Width, part.BoundingBox.Width, 4); - Assert.Equal(originalBbox.Length, part.BoundingBox.Length, 4); - } - - [Fact] - public void Assign_ProgramGeometryMatchesRotatedOriginal() - { - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 2; - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - - // Get the original rotated geometry - var originalGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program); - var originalNonRapid = originalGeometry.Where(e => e.Layer != SpecialLayers.Rapid).ToList(); - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - // The lead-in program should produce geometry that contains the - // original rotated shape (plus lead-in/out extensions) - var leadInGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program); - var leadInNonRapid = leadInGeometry - .Where(e => - e.Layer != SpecialLayers.Rapid - && e.Layer != SpecialLayers.Leadin - && e.Layer != SpecialLayers.Leadout - ) - .ToList(); - - // The bounding box of the cut geometry should be close to original - var origBbox = GetEntityBounds(originalNonRapid); - var leadBbox = GetEntityBounds(leadInNonRapid); - - Assert.Equal(origBbox.Width, leadBbox.Width, 2); - Assert.Equal(origBbox.Length, leadBbox.Length, 2); - } - - [Fact] - public void Assign_MultipleRotatedParts_AllPreserveRotation() - { - var drawing = MakeSquareDrawing(); - var rotations = new[] { 0.0, System.Math.PI / 6, System.Math.PI / 4, System.Math.PI / 2 }; - - var plate = new Plate(60, 120); - for (var i = 0; i < rotations.Length; i++) - { - var part = Part.CreateAtOrigin(drawing, rotations[i]); - part.Offset(10 + i * 15, 10); - plate.Parts.Add(part); - } - - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - for (var i = 0; i < rotations.Length; i++) - Assert.Equal(rotations[i], plate.Parts[i].Rotation, 6); - } - - [Fact] - public void Assign_Twice_PreservesRotation() - { - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 3; // 60 degrees - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - Assert.Equal(rotation, part.Rotation, 6); - - // Assign again (this removes first, then re-applies) - assigner.Assign(plate); - Assert.Equal(rotation, part.Rotation, 6); - } - - [Fact] - public void Assign_AfterExternalHasManualLeadIns_PreservesRotation() - { - // Simulates loading a nest where HasManualLeadIns was saved as true - // but the program doesn't actually contain lead-in codes. - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 2; - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - part.HasManualLeadIns = true; // externally set (e.g., by NestReader) - - var plate = new Plate(60, 120); - plate.Parts.Add(part); - plate.CuttingParameters = new CuttingParameters - { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - }; - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - assigner.Assign(plate); - - Assert.Equal(rotation, part.Rotation, 6); - } - - [Fact] - public void RemoveLeadIns_AfterExternalHasManualLeadIns_PreservesRotation() - { - // Simulates the case where HasManualLeadIns is set externally - // and then lead-ins are removed. - var drawing = MakeSquareDrawing(); - var rotation = System.Math.PI / 3; - var part = Part.CreateAtOrigin(drawing, rotation); - part.Offset(10, 10); - part.HasManualLeadIns = true; - - part.RemoveLeadIns(); - - Assert.Equal(rotation, part.Rotation, 6); - } - - private static Drawing MakeSquareDrawing() - { - var pgm = new Program(); - pgm.Codes.Add(new RapidMove(new Vector(0, 0))); - pgm.Codes.Add(new LinearMove(new Vector(0, 10))); - pgm.Codes.Add(new LinearMove(new Vector(10, 10))); - pgm.Codes.Add(new LinearMove(new Vector(10, 0))); - pgm.Codes.Add(new LinearMove(new Vector(0, 0))); - return new Drawing("test", pgm); - } - - private static Box GetEntityBounds(List entities) - { - double minX = double.MaxValue, - minY = double.MaxValue; - double maxX = double.MinValue, - maxY = double.MinValue; - - foreach (var entity in entities) - { - if (entity is OpenNest.Geometry.Line line) - { - UpdateBounds(line.StartPoint, ref minX, ref minY, ref maxX, ref maxY); - UpdateBounds(line.EndPoint, ref minX, ref minY, ref maxX, ref maxY); - } - } - - return new Box(minX, minY, maxX - minX, maxY - minY); - } - - private static void UpdateBounds( - Vector pt, - ref double minX, - ref double minY, - ref double maxX, - ref double maxY - ) - { - if (pt.X < minX) - minX = pt.X; - if (pt.Y < minY) - minY = pt.Y; - if (pt.X > maxX) - maxX = pt.X; - if (pt.Y > maxY) - maxY = pt.Y; - } -} diff --git a/OpenNest.Tests/IO/PartLeadInSerializationTests.cs b/OpenNest.Tests/IO/PartLeadInSerializationTests.cs index 41aae68a..51b8af1c 100644 --- a/OpenNest.Tests/IO/PartLeadInSerializationTests.cs +++ b/OpenNest.Tests/IO/PartLeadInSerializationTests.cs @@ -34,7 +34,7 @@ public class PartLeadInSerializationTests } [Fact] - public void SingleManualLeadIn_LockedPartIsSkippedByAssignerAfterReload() + public void SingleManualLeadIn_StaysLockedAfterReload() { var nest = CreateNest(); var part = nest.Plates[0].Parts[0]; @@ -46,14 +46,9 @@ public class PartLeadInSerializationTests var loaded = Read(Save(nest)); var plate = loaded.Plates[0]; var restored = plate.Parts[0]; - var program = restored.Program; - plate.CuttingParameters = Parameters(); - - new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate); Assert.True(restored.HasManualLeadIns); Assert.True(restored.LeadInsLocked); - Assert.Same(program, restored.Program); } [Fact] diff --git a/OpenNest.Tests/Support/SupportReviewTests.cs b/OpenNest.Tests/Support/SupportReviewTests.cs new file mode 100644 index 00000000..1ec08d0c --- /dev/null +++ b/OpenNest.Tests/Support/SupportReviewTests.cs @@ -0,0 +1,124 @@ +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Diagnostics; +using OpenNest.Engine.CuttingPlanning; +using OpenNest.Geometry; +using OpenNest.Support; +using OpenNest.Tests.CuttingPlanning; + +namespace OpenNest.Tests.Support; + +/// +/// Parts the support grid cannot hold level: hand-placed lead-ins tab them, Plan Cutting warns +/// when it cannot steady them, and the pre-post review warns about any without a steadying tab. +/// +public class SupportReviewTests +{ + // One row of grid points at y = 2.25 runs under the 10 x 1 strip at (2, 2), below its center. + private static readonly SupportGrid OneRow = new(2, 4, 0, 2.25, stabilityMargin: 0.1); + + private static CuttingParameters Parameters(bool tabs = false) => new() + { + ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 90 }, + SupportTabs = true, + TabsEnabled = tabs, + TabConfig = new NormalTab { Size = 0.2 }, + }; + + private static (Nest Nest, Plate Plate, Part Part) Strip(SupportBed bed, double height = 1) + { + var nest = new Nest("support"); + nest.SetSupportBed(bed); + var plate = new Plate(20, 20) { CuttingParameters = Parameters() }; + nest.Plates.Add(plate); + var part = new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 10, height)), new Vector(2, 2)); + plate.Parts.Add(part); + return (nest, plate, part); + } + + private static void PlaceLeadIn(Part part, CuttingParameters parameters, Vector point, Line edge) => + part.ApplySingleLeadIn(parameters, point, edge, ContourType.External); + + [Fact] + public void ManualLeadIn_TabsAnUnsupportedPartWithoutChangingTheSettings() + { + var (_, plate, part) = Strip(OneRow); + var parameters = Parameters(); + + var used = SupportTabSettings.ForManualLeadIn(parameters, plate, part); + + Assert.NotSame(parameters, used); + Assert.True(used.TabsEnabled); + Assert.False(parameters.TabsEnabled); + } + + [Fact] + public void ManualLeadIn_KeepsTheSettingsForASupportedPartOrWithSupportTabsOff() + { + var (_, plate, part) = Strip(new SupportGrid(0.5, 0.5, 0.25, 0.25, stabilityMargin: 0.1)); + var parameters = Parameters(); + Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, plate, part)); + + var (_, tipping, strip) = Strip(OneRow); + parameters.SupportTabs = false; + Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, tipping, strip)); + } + + [Fact] + public void PrePost_WarnsAboutAnUnsupportedPartWithoutATab() + { + var (nest, _, part) = Strip(OneRow); + PlaceLeadIn(part, Parameters(), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1))); + + var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings, + f => f.Kind == PostVerificationKind.PartSupport); + + Assert.Equal(1, finding.PartNumber); + Assert.Contains("no tab", finding.Message); + Assert.Equal(new Vector(7, 2.5), finding.Location); + } + + [Fact] + public void PrePost_AcceptsATabThatSteadiesThePart_AndWarnsAboutOneThatDoesNot() + { + var (steadied, _, top) = Strip(OneRow); + PlaceLeadIn(top, Parameters(tabs: true), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1))); + var (unsteady, _, bottom) = Strip(OneRow); + PlaceLeadIn(bottom, Parameters(tabs: true), new Vector(5, 0), new Line(new Vector(10, 0), new Vector(0, 0))); + + Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(steadied).Findings, + f => f.Kind == PostVerificationKind.PartSupport); + var finding = Assert.Single(PostVerificationAnalyzer.Analyze(unsteady).Findings, + f => f.Kind == PostVerificationKind.PartSupport); + Assert.Contains("does not steady it", finding.Message); + } + + [Fact] + public void PrePost_IgnoresPartsWithoutSupportTabsOrABed() + { + var (nest, _, part) = Strip(OneRow); + var parameters = Parameters(); + parameters.SupportTabs = false; + // A placed lead-in keeps its settings on the part, which the review reads first. + PlaceLeadIn(part, parameters, new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1))); + Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport); + + parameters.SupportTabs = true; + nest.SetSupportBed(null); + Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport); + } + + [Fact] + public void PlanCutting_WarnsWhenNoSingleTabCanSteadyThePart() + { + // The 10 x 0.8 strip lies between grid rows: it rests on nothing, so a tab only hangs it. + var (_, plate, _) = Strip(new SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1), height: 0.8); + + var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero, + confirmedParameters: Parameters())); + + Assert.Equal(CuttingPlanStatus.BestEffort, result.Status); + var finding = Assert.Single(result.Findings, f => f.Kind == PostVerificationKind.PartSupport); + Assert.Contains("no single tab can steady it", finding.Message); + } +} diff --git a/OpenNest.Tests/TabbedCutouts/TabbedCutoutBuilderTests.cs b/OpenNest.Tests/TabbedCutouts/TabbedCutoutBuilderTests.cs index 8ed7f592..ffa3f641 100644 --- a/OpenNest.Tests/TabbedCutouts/TabbedCutoutBuilderTests.cs +++ b/OpenNest.Tests/TabbedCutouts/TabbedCutoutBuilderTests.cs @@ -220,7 +220,7 @@ public class TabbedCutoutBuilderTests } [Fact] - public void AssignLeadIns_EntersEachPieceFromTheSlug_WithNoLeadOut() + public void ApplyLeadIns_EntersEachPieceFromTheSlug_WithNoLeadOut() { var (entities, square, _) = Part(); var drawing = Build(entities, On(square)); diff --git a/OpenNest.Tests/TestLeadIns.cs b/OpenNest.Tests/TestLeadIns.cs new file mode 100644 index 00000000..ffee8973 --- /dev/null +++ b/OpenNest.Tests/TestLeadIns.cs @@ -0,0 +1,57 @@ +using OpenNest.CNC; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +/// +/// Gives every unlocked part on a plate its lead-ins from the plate's cutting parameters, left +/// to right, the way the retired Assign Lead-ins command did. Post fixtures use it to build +/// programs with lead-ins without running Plan Cutting, so their expected output is unchanged. +/// +internal static class TestLeadIns +{ + public static void Apply(Plate plate) + { + var parameters = plate.CuttingParameters; + if (parameters == null) + return; + + var sequenced = new LeftSideSequencer().Sequence(plate.Parts.ToList(), plate); + var exitPoint = PlateHelper.GetExitPoint(plate); + // The first pass fixes each part's pierce; the second faces each part toward the next pierce. + var piercePoints = Pass(null); + Pass(piercePoints); + + Vector[] Pass(Vector[] nextPiercePoints) + { + var pierces = new Vector[sequenced.Count]; + var current = exitPoint; + for (var i = 0; i < sequenced.Count; i++) + { + var part = sequenced[i].Part; + if (!part.LeadInsLocked) + { + if (part.HasManualLeadIns) + part.RemoveLeadIns(); + var approach = current - part.Location; + if (nextPiercePoints != null && i + 1 < sequenced.Count) + part.ApplyLeadIns(parameters, approach, nextPiercePoints[i + 1] - part.Location); + else + part.ApplyLeadIns(parameters, approach); + } + pierces[i] = Pierce(part); + current = part.Location; + } + return pierces; + } + } + + private static Vector Pierce(Part part) + { + foreach (var code in part.Program.Codes) + if (code is Motion motion) + return motion.EndPoint + part.Location; + return part.Location; + } +} diff --git a/OpenNest.WinForms.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs b/OpenNest.WinForms.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs index 780b2baf..82ab755a 100644 --- a/OpenNest.WinForms.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs +++ b/OpenNest.WinForms.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs @@ -6,31 +6,31 @@ namespace OpenNest.Tests.CuttingStrategy; public class CuttingParametersSerializerTests { [Fact] - public void RoundTrip_PreservesAutoTabFields() + public void RoundTrip_PreservesSupportTabs() { var original = new CuttingParameters { - AutoTabMinSize = 0.5, - AutoTabMaxSize = 3.0, + SupportTabs = true, ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, }; var json = CuttingParametersSerializer.Serialize(original); var restored = CuttingParametersSerializer.Deserialize(json); - Assert.Equal(0.5, restored.AutoTabMinSize); - Assert.Equal(3.0, restored.AutoTabMaxSize); + Assert.True(restored.SupportTabs); } [Fact] - public void Deserialize_MissingAutoTabFields_DefaultsToZero() + public void Deserialize_RetiredAutoTabFields_AreIgnored() { + // Settings saved before Auto Tab was removed still load; its sizes are dropped. var json = - "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}"; + "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625,\"autoTabMinSize\":0.5,\"autoTabMaxSize\":3.0}"; var restored = CuttingParametersSerializer.Deserialize(json); - Assert.Equal(0.0, restored.AutoTabMinSize); - Assert.Equal(0.0, restored.AutoTabMaxSize); + Assert.False(restored.TabsEnabled); + Assert.False(restored.SupportTabs); + Assert.Equal(0.0625, restored.PierceClearance); } } diff --git a/OpenNest/Actions/ActionLeadIn.cs b/OpenNest/Actions/ActionLeadIn.cs index 601cf74b..79e989ab 100644 --- a/OpenNest/Actions/ActionLeadIn.cs +++ b/OpenNest/Actions/ActionLeadIn.cs @@ -95,8 +95,7 @@ namespace OpenNest.Actions if (form == null) return; - cuttingPanel = new CuttingPanel { ShowAutoAssign = true }; - cuttingPanel.AutoAssignClicked += OnAutoAssignClicked; + cuttingPanel = new CuttingPanel(); cuttingPanel.ParametersChanged += OnToolParametersChanged; // Load current parameters or defaults @@ -130,7 +129,6 @@ namespace OpenNest.Actions SaveParameters(); cuttingPanel.ParametersChanged -= OnToolParametersChanged; - cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked; var form = plateView.FindForm() as EditNestForm; form?.HideSidePanel(); @@ -492,13 +490,12 @@ namespace OpenNest.Actions private void CommitLeadIn() { - var parameters = GetCurrentParameters(); - if (selectedPart.HasManualLeadIns) selectedPart.RemoveLeadIns(); - ApplyAutoTab(parameters); - + // A part the support grid cannot hold level is tabbed at the clicked entry. + var parameters = Support.SupportTabSettings.ForManualLeadIn(GetCurrentParameters(), + plateView.Plate, selectedPart); selectedPart.ApplySingleLeadIn(parameters, snapPoint, snapEntity, snapContourType); selectedPart.LeadInsLocked = true; @@ -507,38 +504,6 @@ namespace OpenNest.Actions plateView.Invalidate(); } - private void OnAutoAssignClicked(object sender, System.EventArgs e) - { - if (selectedPart == null) - return; - - var parameters = GetCurrentParameters(); - - if (selectedPart.HasManualLeadIns) - selectedPart.RemoveLeadIns(); - - ApplyAutoTab(parameters); - - selectedPart.ApplyLeadIns(parameters, Vector.Zero); - selectedPart.LeadInsLocked = true; - - selectedLayoutPart.IsDirty = true; - UnlockContour(); - plateView.Invalidate(); - } - - private void ApplyAutoTab(CuttingParameters parameters) - { - if (parameters.AutoTabMinSize <= 0 && parameters.AutoTabMaxSize <= 0) - return; - - var bbox = selectedPart.Program.BoundingBox(); - var minDim = System.Math.Min(bbox.Width, bbox.Length); - - if (minDim >= parameters.AutoTabMinSize && minDim <= parameters.AutoTabMaxSize) - parameters.TabsEnabled = true; - } - private void DeselectPart() { if (selectedLayoutPart != null) diff --git a/OpenNest/Controls/CuttingPanel.cs b/OpenNest/Controls/CuttingPanel.cs index 06521199..c45a763d 100644 --- a/OpenNest/Controls/CuttingPanel.cs +++ b/OpenNest/Controls/CuttingPanel.cs @@ -37,28 +37,16 @@ namespace OpenNest.Controls private readonly CheckBox chkTabsEnabled; private readonly NumericUpDown nudTabWidth; private readonly RadioButton rbTabAll; - private readonly RadioButton rbAutoTab; private readonly RadioButton rbSupportTab; - private readonly NumericUpDown nudAutoTabMin; - private readonly NumericUpDown nudAutoTabMax; private readonly NumericUpDown nudPierceClearance; private readonly NumericUpDown nudPierceDiameter; private readonly CheckBox chkRoundLeadInAngles; private readonly NumericUpDown nudLeadInAngleIncrement; - private readonly Button btnAutoAssign; - private bool suppressEvents; public event EventHandler ParametersChanged; - public event EventHandler AutoAssignClicked; - - public bool ShowAutoAssign - { - get => btnAutoAssign.Visible; - set => btnAutoAssign.Visible = value; - } public ContourType? ActiveContourType { @@ -136,7 +124,7 @@ namespace OpenNest.Controls { HeaderText = "Tabs", Dock = DockStyle.Top, - ExpandedHeight = 160, + ExpandedHeight = 106, IsExpanded = false, }; @@ -171,46 +159,11 @@ namespace OpenNest.Controls }; tabsPanel.ContentPanel.Controls.Add(rbTabAll); - rbAutoTab = new RadioButton - { - Text = "Auto-tab when smallest part dimension is between:", - Location = new Point(28, 50), - AutoSize = true, - Enabled = false, - }; - tabsPanel.ContentPanel.Controls.Add(rbAutoTab); - - tabsPanel.ContentPanel.Controls.Add( - new Label - { - Text = "Min:", - Location = new Point(44, 76), - AutoSize = true, - } - ); - - nudAutoTabMin = CreateNumeric(77, 73, 0, 0.0625); - nudAutoTabMin.Enabled = false; - tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin); - - tabsPanel.ContentPanel.Controls.Add( - new Label - { - Text = "Max:", - Location = new Point(210, 76), - AutoSize = true, - } - ); - - nudAutoTabMax = CreateNumeric(245, 73, 0, 0.0625); - nudAutoTabMax.Enabled = false; - tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax); - // Plan Cutting tabs only the parts the plate's support grid cannot hold level. rbSupportTab = new RadioButton { Text = "Tab parts the support grid cannot hold level", - Location = new Point(28, 102), + Location = new Point(28, 50), AutoSize = true, Enabled = false, }; @@ -221,27 +174,12 @@ namespace OpenNest.Controls var enabled = chkTabsEnabled.Checked; nudTabWidth.Enabled = enabled; rbTabAll.Enabled = enabled; - rbAutoTab.Enabled = enabled; rbSupportTab.Enabled = enabled; - nudAutoTabMin.Enabled = enabled && rbAutoTab.Checked; - nudAutoTabMax.Enabled = enabled && rbAutoTab.Checked; OnParametersChanged(); }; - rbTabAll.CheckedChanged += (s, e) => - { - nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked; - nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked; - OnParametersChanged(); - }; - - // Switching between auto and support tabs does not pass through Tab all parts. - rbSupportTab.CheckedChanged += (s, e) => - { - nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked; - nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked; - OnParametersChanged(); - }; + // The two choices toggle together, so one handler sees every change. + rbTabAll.CheckedChanged += (s, e) => OnParametersChanged(); // Pierce section var piercePanel = new CollapsiblePanel @@ -308,26 +246,7 @@ namespace OpenNest.Controls nudLeadInAngleIncrement.ValueChanged += (s, e) => OnParametersChanged(); piercePanel.ContentPanel.Controls.Add(nudLeadInAngleIncrement); - // Auto-Assign button — wrapped in a panel for Dock.Top with padding - btnAutoAssign = new Button - { - Text = "Auto-Assign Lead-ins", - Dock = DockStyle.Top, - Height = 32, - Visible = false, - }; - btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty); - - var btnWrapper = new Panel - { - Dock = DockStyle.Top, - Height = 36, - Padding = new Padding(4, 2, 4, 2), - }; - btnWrapper.Controls.Add(btnAutoAssign); - // Add in reverse order — Dock.Top stacks top-down - Controls.Add(btnWrapper); Controls.Add(piercePanel); Controls.Add(tabsPanel); Controls.Add(tabWrapper); @@ -353,10 +272,6 @@ namespace OpenNest.Controls PierceDiameter = (double)nudPierceDiameter.Value, RoundLeadInAngles = chkRoundLeadInAngles.Checked, LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value, - AutoTabMinSize = - chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0, - AutoTabMaxSize = - chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0, SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked, }; } @@ -372,11 +287,9 @@ namespace OpenNest.Controls LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn); LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut); - var hasAutoTab = p.AutoTabMinSize > 0 || p.AutoTabMaxSize > 0; - chkTabsEnabled.Checked = p.TabsEnabled || hasAutoTab || p.SupportTabs; - rbAutoTab.Checked = hasAutoTab && !p.SupportTabs; + chkTabsEnabled.Checked = p.TabsEnabled || p.SupportTabs; rbSupportTab.Checked = p.SupportTabs; - rbTabAll.Checked = !hasAutoTab && !p.SupportTabs; + rbTabAll.Checked = !p.SupportTabs; if (p.TabConfig != null) nudTabWidth.Value = (decimal)p.TabConfig.Size; nudPierceClearance.Value = (decimal)p.PierceClearance; @@ -384,8 +297,6 @@ namespace OpenNest.Controls chkRoundLeadInAngles.Checked = p.RoundLeadInAngles; nudLeadInAngleIncrement.Value = (decimal)p.LeadInAngleIncrement; nudLeadInAngleIncrement.Enabled = p.RoundLeadInAngles; - nudAutoTabMin.Value = (decimal)p.AutoTabMinSize; - nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize; suppressEvents = false; } diff --git a/OpenNest/Controls/SupportOverlay.cs b/OpenNest/Controls/SupportOverlay.cs index 2cb673b1..579a0ce9 100644 --- a/OpenNest/Controls/SupportOverlay.cs +++ b/OpenNest/Controls/SupportOverlay.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Drawing; +using System.Drawing.Drawing2D; using System.Runtime.CompilerServices; using OpenNest.Geometry; using OpenNest.Support; @@ -18,6 +19,9 @@ namespace OpenNest.Controls // Grid points are drawn only while they are at least this far apart on screen. private const float MinimumGridSpacing = 6f; + // Grid points are this many whole pixels square. + private const float GridPointSize = 3f; + private static readonly Color GridColor = Color.FromArgb(110, 120, 120, 120); private static readonly Color ContactColor = Color.FromArgb(220, 30, 150, 60); private static readonly Color SupportedColor = Color.FromArgb(230, 20, 140, 50); @@ -80,8 +84,32 @@ namespace OpenNest.Controls var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0; var sheet = new Box(left, bottom, plate.Size.Length, plate.Size.Width); using var brush = new SolidBrush(GridColor); - foreach (var point in grid.PointsIn(sheet, plate.Quadrant)) - Dot(g, view, point, 1.5f, brush); + // Anti-aliased dots smear differently at each subpixel offset; whole-pixel squares + // drawn without smoothing are all the same size. + // The view is translated by a fractional origin, so snap in device pixels. + using var transform = g.Transform; + var dx = transform.OffsetX; + var dy = transform.OffsetY; + var half = (GridPointSize - 1) / 2; + var smoothing = g.SmoothingMode; + var offset = g.PixelOffsetMode; + g.SmoothingMode = SmoothingMode.None; + g.PixelOffsetMode = PixelOffsetMode.Half; + try + { + foreach (var point in grid.PointsIn(sheet, plate.Quadrant)) + { + var p = view.PointWorldToGraph(point); + var x = (float)System.Math.Round(p.X + dx) - dx - half; + var y = (float)System.Math.Round(p.Y + dy) - dy - half; + g.FillRectangle(brush, x, y, GridPointSize, GridPointSize); + } + } + finally + { + g.SmoothingMode = smoothing; + g.PixelOffsetMode = offset; + } } private static void Dot(Graphics g, PlateView view, Vector point, float radius, Brush brush) diff --git a/OpenNest/Forms/CuttingInspectionPanel.cs b/OpenNest/Forms/CuttingInspectionPanel.cs index 756ef874..bcda1275 100644 --- a/OpenNest/Forms/CuttingInspectionPanel.cs +++ b/OpenNest/Forms/CuttingInspectionPanel.cs @@ -550,6 +550,7 @@ internal sealed class CuttingInspectionPanel : UserControl PostVerificationKind.NestedLead => "Nested part lead", PostVerificationKind.PierceZone => "Pierce zone", PostVerificationKind.PartSpacing => "Part spacing", + PostVerificationKind.PartSupport => "Part support", _ => kind.ToString(), }; @@ -615,6 +616,7 @@ internal sealed class CuttingInspectionPanel : UserControl PostVerificationKind.NestedLead => "The ring marks the middle of the lead that meets the other part.", PostVerificationKind.PierceZone => "The ring marks the pierce that is too close to part material.", PostVerificationKind.PartSpacing => "The ring marks where the two parts come closest.", + PostVerificationKind.PartSupport => "The ring marks the part's center of gravity.", _ => "The ring marks the affected part; the exact spot is not known." }; diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 1c36fa40..8a0ad1cd 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1112,35 +1112,6 @@ namespace OpenNest.Forms CalculateCurrentPlateCutTime(); } - public void AssignLeadIns_Click(object sender, EventArgs e) - { - if (PlateView?.Plate == null) - return; - - var plate = PlateView.Plate; - - var parameters = LoadOrDefaultParameters(plate.CuttingParameters); - - using var dlg = new CuttingParametersDialog(); - dlg.LoadParameters(parameters); - - if (dlg.ShowDialog() != DialogResult.OK) - return; - - parameters = dlg.GetParameters(); - plate.CuttingParameters = parameters; - SaveCuttingParameters(parameters); - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - var kept = assigner.Assign(plate); - - foreach (var lp in PlateView.Parts) - lp.IsDirty = true; - - PlateView.Invalidate(); - ShowKeptDrawingLeadIns([(Nest.Plates.IndexOf(plate) + 1, PartNumbers(plate, kept))]); - } - public void RemoveLeadIns_Click(object sender, EventArgs e) { if (PlateView?.Plate == null) @@ -1173,47 +1144,6 @@ namespace OpenNest.Forms PlateView.Invalidate(); } - public void AssignLeadInsAllPlates() - { - if (Nest == null) - return; - - var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters); - - using var dlg = new CuttingParametersDialog(); - dlg.LoadParameters(parameters); - - if (dlg.ShowDialog() != DialogResult.OK) - return; - - parameters = dlg.GetParameters(); - SaveCuttingParameters(parameters); - - var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; - var kept = new List<(int, IReadOnlyList)>(); - - for (var index = 0; index < Nest.Plates.Count; index++) - { - var plate = Nest.Plates[index]; - plate.CuttingParameters = parameters; - kept.Add((index + 1, PartNumbers(plate, assigner.Assign(plate)))); - } - - PlateView.Invalidate(); - ShowKeptDrawingLeadIns(kept); - } - - // Assign Lead-ins never overwrites drawing pins; say which parts it left for Plan Cutting. - private void ShowKeptDrawingLeadIns(IReadOnlyList<(int PlateNumber, IReadOnlyList PartNumbers)> kept) - { - var message = LeadInAssigner.KeptMessage(kept); - if (message != null) - MessageBox.Show(this, message, "Assign Lead-ins", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - private static IReadOnlyList PartNumbers(Plate plate, IReadOnlyList parts) => - parts.Select(part => plate.Parts.IndexOf(part) + 1).Order().ToList(); - public void RemoveLeadInsAllPlates() { if (Nest == null) diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index acf85830..5f71f76d 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -112,7 +112,6 @@ namespace OpenNest.Forms toolStripMenuItem22 = new System.Windows.Forms.ToolStripSeparator(); mnuNestAutomaticCutOff = new System.Windows.Forms.ToolStripMenuItem(); mnuNestPlanCutting = new System.Windows.Forms.ToolStripMenuItem(); - mnuNestAssignLeadIns = new System.Windows.Forms.ToolStripMenuItem(); mnuNestRemoveLeadIns = new System.Windows.Forms.ToolStripMenuItem(); mnuPlate = new System.Windows.Forms.ToolStripMenuItem(); mnuPlateEdit = new System.Windows.Forms.ToolStripMenuItem(); @@ -152,7 +151,6 @@ namespace OpenNest.Forms mnuPlateSimulateCutting = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem21 = new System.Windows.Forms.ToolStripSeparator(); mnuPlatePlanCutting = new System.Windows.Forms.ToolStripMenuItem(); - mnuPlateAssignLeadIns = new System.Windows.Forms.ToolStripMenuItem(); mnuPlatePlaceLeadIn = new System.Windows.Forms.ToolStripMenuItem(); mnuPlateRemoveLeadIns = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem20 = new System.Windows.Forms.ToolStripSeparator(); @@ -585,7 +583,7 @@ namespace OpenNest.Forms // // mnuNest // - mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns }); + mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestRemoveLeadIns }); mnuNest.Name = "mnuNest"; mnuNest.Size = new System.Drawing.Size(43, 20); mnuNest.Text = "&Nest"; @@ -755,13 +753,6 @@ namespace OpenNest.Forms mnuNestPlanCutting.Text = "Plan Cutting (All Plates)..."; mnuNestPlanCutting.Click += NestPlanCutting_Click; // - // mnuNestAssignLeadIns - // - mnuNestAssignLeadIns.Name = "mnuNestAssignLeadIns"; - mnuNestAssignLeadIns.Size = new System.Drawing.Size(205, 22); - mnuNestAssignLeadIns.Text = "Assign Lead-ins (All Plates)"; - mnuNestAssignLeadIns.Click += NestAssignLeadIns_Click; - // // mnuNestRemoveLeadIns // mnuNestRemoveLeadIns.Name = "mnuNestRemoveLeadIns"; @@ -771,7 +762,7 @@ namespace OpenNest.Forms // // mnuPlate // - mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateEditClamps, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateArrange, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, mnuPlateSimulateCutting, toolStripMenuItem21, mnuPlatePlanCutting, mnuPlateAssignLeadIns, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns, toolStripMenuItem20, mnuPlateViewInCad }); + mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateEditClamps, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateArrange, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, mnuPlateSimulateCutting, toolStripMenuItem21, mnuPlatePlanCutting, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns, toolStripMenuItem20, mnuPlateViewInCad }); mnuPlate.Name = "mnuPlate"; mnuPlate.Size = new System.Drawing.Size(45, 20); mnuPlate.Text = "&Plate"; @@ -1043,13 +1034,6 @@ namespace OpenNest.Forms mnuPlatePlanCutting.Text = "Plan Cutting..."; mnuPlatePlanCutting.Click += PlatePlanCutting_Click; // - // mnuPlateAssignLeadIns - // - mnuPlateAssignLeadIns.Name = "mnuPlateAssignLeadIns"; - mnuPlateAssignLeadIns.Size = new System.Drawing.Size(177, 22); - mnuPlateAssignLeadIns.Text = "Assign Lead-ins"; - mnuPlateAssignLeadIns.Click += PlateAssignLeadIns_Click; - // // mnuPlatePlaceLeadIn // mnuPlatePlaceLeadIn.Name = "mnuPlatePlaceLeadIn"; @@ -1479,13 +1463,11 @@ namespace OpenNest.Forms private System.Windows.Forms.ToolStripMenuItem mnuPlateAutomaticCutOff; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem21; private System.Windows.Forms.ToolStripMenuItem mnuPlatePlanCutting; - private System.Windows.Forms.ToolStripMenuItem mnuPlateAssignLeadIns; private System.Windows.Forms.ToolStripMenuItem mnuPlatePlaceLeadIn; private System.Windows.Forms.ToolStripMenuItem mnuPlateRemoveLeadIns; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem22; private System.Windows.Forms.ToolStripMenuItem mnuNestAutomaticCutOff; private System.Windows.Forms.ToolStripMenuItem mnuNestPlanCutting; - private System.Windows.Forms.ToolStripMenuItem mnuNestAssignLeadIns; private System.Windows.Forms.ToolStripMenuItem mnuNestRemoveLeadIns; } } \ No newline at end of file diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index 7574700c..2b6a9bee 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -263,8 +263,6 @@ namespace OpenNest.Forms private void CalculateNestCutTime_Click(object sender, EventArgs e) => WithActive(f => f.CalculateNestCutTime()); - private void NestAssignLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.AssignLeadInsAllPlates()); - private void NestRemoveLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.RemoveLeadInsAllPlates()); #endregion Nest Menu Events diff --git a/OpenNest/Forms/MainForm.Plate.cs b/OpenNest/Forms/MainForm.Plate.cs index c369b064..874120b5 100644 --- a/OpenNest/Forms/MainForm.Plate.cs +++ b/OpenNest/Forms/MainForm.Plate.cs @@ -150,8 +150,6 @@ namespace OpenNest.Forms } } - private void PlateAssignLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.AssignLeadIns_Click(sender, e)); - private void PlatePlaceLeadIn_Click(object sender, EventArgs e) => WithActive(f => f.PlaceLeadIn_Click(sender, e)); private void PlateRemoveLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.RemoveLeadIns_Click(sender, e)); diff --git a/docs/breakoffs.md b/docs/breakoffs.md index e3404b8a..03ab4201 100644 --- a/docs/breakoffs.md +++ b/docs/breakoffs.md @@ -40,7 +40,7 @@ material capture, overlap checks, nesting and area ignore them. `ShapeProfile` a `ConvertGeometry` pass them through in order without re-chaining or re-winding. Both Plan Cutting (`PreparedContours`, emitted after scribes and before contours in -every full or prefix emission, never in single-contour probes) and Assign Lead-ins +every full or prefix emission, never in single-contour probes) and the manual lead-in tool (`ContourCuttingStrategy.Apply`/`ApplySingle`) emit them through `ContourCuttingStrategy.EmitBreakCuts`: diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index dd68cb61..3319bc28 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -7,7 +7,7 @@ returns an owned proposal; `Apply` installs Ready plate-scoped proposals atomica after an exact freshness check. The desktop opens it from `Plate > Plan Cutting...` and `Nest > Plan Cutting (All Plates)...` (see [Desktop workflow](#desktop-workflow)); the console and MCP server expose the same batch (see [Console and MCP](#console-and-mcp)); the older -automatic sequencing and lead-in assignment commands remain until they are retired. +automatic sequencing command remains until it is retired; the older lead-in assignment was removed. ## Capture before worker planning @@ -486,10 +486,9 @@ clean contours with `CutPinResolver` and prepares them with - The plate state compared by `Apply` includes each drawing's pin list (reference and exact content, `StateFingerprint.Of(pins)`) and each part's pins-off flag, so changing either after capture makes the proposal `Stale`. -- Only Plan Cutting applies pins. The legacy Assign Lead-ins (`LeadInAssigner`) leaves every - unlocked part with active pins unchanged and returns those parts; the desktop then says - "N parts kept their drawing lead-ins (plate P: parts ...); use Plan Cutting to apply their - drawing pins." It never overwrites a pin with the plate's settings. +- Only Plan Cutting applies pins. Plan Cutting is the only automatic lead-in assignment: the + legacy Assign Lead-ins commands, the lead-in tool's Auto-Assign and the Auto Tab size + range were removed. - The [pre-post review](post-verification.md) warns "Drawing pins not applied" for a part with active pins whose placed program does not cut its pinned contours as pinned, and re-checks pinned leads like locked ones. @@ -737,7 +736,6 @@ The service does not establish clean-material non-overlap, scrap release by open cutoff cuts or sheet edges, or physical retention strength. It does not write CNC or set posting consent. A `Ready` proposal can still be unsuitable for cutting. -The lead-in side panel's Auto-Assign Lead-ins remains separate from Plan Cutting. Review the desktop preview and the generated CNC output before cutting. Fresh [pre-post verification](post-verification.md) is still required; it is not a physical safety qualification. diff --git a/docs/geometry/lead-in-placement.md b/docs/geometry/lead-in-placement.md index dad820ed..ae0801c0 100644 --- a/docs/geometry/lead-in-placement.md +++ b/docs/geometry/lead-in-placement.md @@ -62,7 +62,7 @@ agreement, approach-angle handling, the flat-corner clearance fallback, reflex notches, and straight lead-outs. Windows manual check: import a part with small rectangular cutouts and use Plate > -Assign Lead-ins. Confirm diagonal lead-ins at the cutout corners. Under Plate > +Plan Cutting. Confirm diagonal lead-ins at the cutout corners. Under Plate > Place Lead-in, select the part, lock a cutout, and hover a corner: the preview should point into the cutout and the committed lead-in should match. Hover a mid-edge point and confirm perpendicular placement is unchanged. Windows visual diff --git a/docs/post-verification.md b/docs/post-verification.md index 268a80dd..4c38d700 100644 --- a/docs/post-verification.md +++ b/docs/post-verification.md @@ -96,6 +96,9 @@ shows the first three check categories, even when there are no findings: are not reported. A part whose clean material cannot be captured makes the check incomplete for leads near it. This line appears in the summary only when there is such a finding. +- **Part support:** on a plate with a support bed, a part set for support tabs that the bed + cannot hold level and whose outside cut leaves no tab that keeps it level; the marker is its + center of gravity. See [support tabs](support-tabs.md#pre-post-review). - **Part spacing:** two parts closer than the plate's part spacing without sharing material (overlapping pairs are reported only as overlaps). Candidate pairs come from each part's clean nesting material (leads excluded) tested exactly as Auto Nest's layout check tests diff --git a/docs/support-tabs.md b/docs/support-tabs.md index b778597e..ef8060ff 100644 --- a/docs/support-tabs.md +++ b/docs/support-tabs.md @@ -71,7 +71,7 @@ other parts keep the plate settings, so supported parts are not tabbed. The emitter tabs a perimeter at its entry: the cut stops short of the entry by the tab size. A support-tabbed perimeter's automatic entries are therefore limited to catalogue points the analysis accepts, plus the closest native point to its best tab point (`AutomaticEntryKind.SupportTab`, -never on a reflex or cusp vertex). Ranking, lead checks and replay are unchanged. +never on a reflex or cusp vertex). Ranking and lead checks are unchanged. If none of those entries has a lead-in that fits, the part is still tabbed. The search falls back first to every catalogue entry clear of inside corners, then to every entry @@ -79,16 +79,39 @@ back first to every catalogue entry clear of inside corners, then to every entry lead-in fits, it keeps every one that holds the part level, or else only the steadiest. The open-scrap pierce fallback then tries the last of those catalogues. -Replay notes each planned part whose tab, at its perimeter entry, cannot hold it level (no single -tab can, or no lead-in fits where one would) or sits in an inside corner -(`SupportTabCapture.Notes`). The notes are advisory: a Ready plan keeps its status and lists them -in Plan Cutting's warnings, and the console and MCP summaries print them under the plate. +A drawing pin on the perimeter wins: its entry, leads and tab are kept as pinned, and the +analysis is only carried for review. Replay and best-effort plans (`SupportTabCapture.Review`) +warn with `PostVerificationKind.PartSupport` whenever an unsupported part's emitted perimeter is +untabbed or its entry does not steady the part: its pin has no tab or a tab elsewhere, no +steadying entry had a lead-in that fits, or no single tab can steady it. Like every planner +warning, this makes the proposal UNVERIFIED, so applying it needs explicit acceptance. + +A planned tab that sits in an inside corner, because no lead-in fits clear of one, gets an +advisory note instead (`SupportTabCapture.Notes`; never for a pin): a Ready plan keeps its +status and lists it in Plan Cutting's warnings, and the console and MCP summaries print it under +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 +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. + +## Pre-post review + +`PartSupportReview` runs in every pre-post verification and in Validate Nest. On a plate with a +support bed, each part whose settings (its own, else the plate's) ask for support tabs and that +the bed cannot hold level 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, 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: -- A drawing pin on the perimeter wins: its entry and tab are kept as pinned. -- Locked or ineligible parts keep their programs; the older Assign Lead-ins command and - Auto Tab sizes ignore the bed. +- Locked or ineligible parts keep their programs in Plan Cutting; the pre-post review still + checks them. - Only one perimeter tab per part. A part that needs more (one tab cannot reach the stability - margin) gets the best single tab and a note. -- Pre-post verification does not yet warn about unsupported, untabbed parts. + margin) gets the best single tab and a warning. diff --git a/docs/tabbed-cutouts.md b/docs/tabbed-cutouts.md index e7f4de50..828e998a 100644 --- a/docs/tabbed-cutouts.md +++ b/docs/tabbed-cutouts.md @@ -41,7 +41,7 @@ best-fit, `NestLayoutCheck` and the GPU bitmaps. Programs without tab spans read before. A planned program has open pieces instead of a closed hole, so it bounds no hole either way. -Both Plan Cutting (`PreparedContours`) and Assign Lead-ins +Both Plan Cutting (`PreparedContours`) and the manual lead-in tool (`ContourCuttingStrategy.Apply`/`ApplySingle`) take a closed contour with tab spans out of the contour choices. Every full or prefix emission cuts its pieces after the break cuts and before the contours, never in single-contour probes: