From 4aa035af2f6358cfb690c2ea24abf6a624f35b85 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 6 Nov 2026 12:28:52 -0500 Subject: [PATCH] feat(diagnostics): mark plate-edge findings on the plate and say "outside the work area" The Layout label counted parts off the plate or inside the edge spacing, but nothing showed which part or where, so the user had to hunt for it. Each edge finding now gets a canvas marker like the overlap and spacing ones: a red crosshair where a part leaves the sheet and an orange ring at the gap to the edge, in every visible display mode, with hover text that carries the finding's message. Markers are retained across edits like the other overlay findings (only a changed part's marker drops), and a sheet size, edge spacing or outline change clears them. Validate Nest already located these findings through PostVerificationFinding.Location. "Near edge" / "inside the edge spacing" read as a spacing rule rather than a placement problem, so the label, overlay messages, PlateEdgeReview findings and the Validate Nest category now say the part is outside the work area. Off the plate keeps its wording. The PostVerificationKind.EdgeSpacing name stays. Co-Authored-By: Claude Opus 5.5 --- .../Diagnostics/OverlapGeometryStamp.cs | 5 ++ .../Diagnostics/OverlapPairPresentation.cs | 18 ++++++ .../Diagnostics/OverlapReportState.cs | 16 ++++- OpenNest.Core/Diagnostics/PlateEdgeReview.cs | 10 ++-- .../Diagnostics/PostVerificationReport.cs | 2 +- .../Diagnostics/OverlapEdgeLabelTests.cs | 48 ++++++++++++++- .../Diagnostics/PlateEdgeReviewTests.cs | 11 ++-- .../PlateOverlapOverlayTests.cs | 32 ++++++++++ OpenNest/Controls/OverlapOverlayController.cs | 58 +++++++++++++++++-- OpenNest/Forms/CuttingInspectionPanel.cs | 4 +- OpenNest/Forms/PostVerificationForm.cs | 2 +- docs/geometry/visual-overlap-check.md | 11 +++- docs/post-verification.md | 14 +++-- 13 files changed, 198 insertions(+), 33 deletions(-) diff --git a/OpenNest.Core/Diagnostics/OverlapGeometryStamp.cs b/OpenNest.Core/Diagnostics/OverlapGeometryStamp.cs index 09c69068..a8c80788 100644 --- a/OpenNest.Core/Diagnostics/OverlapGeometryStamp.cs +++ b/OpenNest.Core/Diagnostics/OverlapGeometryStamp.cs @@ -95,6 +95,11 @@ public sealed class OverlapGeometryStamp ReferenceEquals(plate, current) && partSpacing == Bits(current.PartSpacing) && commonLines == CommonLineState(current); + /// The same sheet, edge spacing and outline: an unchanged part keeps its edge finding. + internal bool SameSheet(Plate current) => + ReferenceEquals(plate, current) && sheet == SheetState(current) && edgeSpacing == EdgeState(current) + && Equals(outline, current.Outline); + // Which plate slots share a common-line group, and its kerf: the spacing check reads both. private static string CommonLineState(Plate plate) => string.Join(";", plate.CommonLines.Select(group => Bits(group.Kerf) + ":" diff --git a/OpenNest.Core/Diagnostics/OverlapPairPresentation.cs b/OpenNest.Core/Diagnostics/OverlapPairPresentation.cs index 978d1fe2..f7931749 100644 --- a/OpenNest.Core/Diagnostics/OverlapPairPresentation.cs +++ b/OpenNest.Core/Diagnostics/OverlapPairPresentation.cs @@ -37,6 +37,18 @@ public static class OverlapPairPresentation + $"Marker: ({Number(pair.Marker.X, provider)}, {Number(pair.Marker.Y, provider)}) {units}"; } + /// A short canvas label: the part's sequence number and what is wrong. + public static string Label(PlateEdgeFinding finding) => + $"{finding.PartId + 1} " + (finding.OffPlate ? "off plate" : "outside work area"); + + public static string Details(PlateEdgeFinding finding, Units capturedUnits, IFormatProvider provider = null) + { + var units = UnitsHelper.GetShortString(capturedUnits); + return (finding.OffPlate ? "Off the plate" : "Outside work area") + $": part {finding.PartId + 1}\n" + + finding.Message + "\n" + + $"Marker: ({Number(finding.Marker.X, provider)}, {Number(finding.Marker.Y, provider)}) {units}"; + } + // Significant figures adapt to scale: a positive sliver must never read as zero. public static string Number(double value, IFormatProvider provider = null) => value.ToString("G6", provider ?? CultureInfo.CurrentCulture); @@ -60,6 +72,12 @@ public static class OverlapPairPresentation pairs.Where(pair => Hits(worldToScreen(pair.Marker), pointer, deviceDpi)) .OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToArray(); + /// + public static IReadOnlyList HitTest(IEnumerable findings, + Func worldToScreen, Vector pointer, int deviceDpi) => + findings.Where(finding => Hits(worldToScreen(finding.Marker), pointer, deviceDpi)) + .OrderBy(finding => finding.PartId).ToArray(); + private static bool Hits(Vector center, Vector pointer, int deviceDpi) { var radius = HitRadius(deviceDpi); diff --git a/OpenNest.Core/Diagnostics/OverlapReportState.cs b/OpenNest.Core/Diagnostics/OverlapReportState.cs index 26f3489c..2917b759 100644 --- a/OpenNest.Core/Diagnostics/OverlapReportState.cs +++ b/OpenNest.Core/Diagnostics/OverlapReportState.cs @@ -26,6 +26,9 @@ public sealed class OverlapReportState /// Known part-spacing pairs safe to draw, retained like . public IReadOnlyList DisplaySpacingPairs { get; private set; } = Array.Empty(); + /// Known plate-edge findings safe to draw, retained like . + public IReadOnlyList DisplayEdgeFindings { get; private set; } = + Array.Empty(); public bool IsRunning => Status == OverlapCheckStatus.Checking; /// A current, fully checked report with no finding of any kind. public bool IsAllClear => Status == OverlapCheckStatus.Current && Report.Pairs.Count == 0 @@ -86,7 +89,7 @@ public sealed class OverlapReportState if (off != 0) words.Add($"{off} off plate"); if (near != 0) - words.Add($"{near} near edge"); + words.Add($"{near} outside work area"); return string.Join(", ", words); } @@ -96,7 +99,7 @@ public sealed class OverlapReportState var off = Report.EdgeFindings.Count(finding => finding.OffPlate); var near = Report.EdgeFindings.Count - off; return (off == 0 ? "" : $"{off} parts off the plate{separator}") - + (near == 0 ? "" : $"{near} parts inside the edge spacing{separator}"); + + (near == 0 ? "" : $"{near} parts outside the work area{separator}"); } // Only a fully checked report that required a gap may clear spacing as well as overlaps. @@ -139,6 +142,7 @@ public sealed class OverlapReportState : report.Pairs.Concat(report.ClampFindings).ToList().AsReadOnly(); DisplayCutOffClampFindings = report.CutOffClampFindings; DisplaySpacingPairs = report.SpacingPairs; + DisplayEdgeFindings = report.EdgeFindings; displayStamp = stamp; observedDisplayStamp = stamp; uncheckedPartCount = CountUncheckedParts(report.Issues.Concat(report.SpacingIssues)); @@ -203,7 +207,12 @@ public sealed class OverlapReportState && unchanged[pair.PartAId] && unchanged[pair.PartBId]).ToList(); if (retainedSpacing.Count != DisplaySpacingPairs.Count) DisplaySpacingPairs = retainedSpacing.AsReadOnly(); - if (DisplayPairs.Count == 0 && DisplayCutOffClampFindings.Count == 0 && DisplaySpacingPairs.Count == 0) + var sameSheet = displayStamp.SameSheet(plate); + var retainedEdges = DisplayEdgeFindings.Where(finding => sameSheet && unchanged[finding.PartId]).ToList(); + if (retainedEdges.Count != DisplayEdgeFindings.Count) + DisplayEdgeFindings = retainedEdges.AsReadOnly(); + if (DisplayPairs.Count == 0 && DisplayCutOffClampFindings.Count == 0 && DisplaySpacingPairs.Count == 0 + && DisplayEdgeFindings.Count == 0) ClearDisplayPairs(); else observedDisplayStamp = OverlapGeometryStamp.Capture(plate); @@ -214,6 +223,7 @@ public sealed class OverlapReportState DisplayPairs = Array.Empty(); DisplayCutOffClampFindings = Array.Empty(); DisplaySpacingPairs = Array.Empty(); + DisplayEdgeFindings = Array.Empty(); displayStamp = null; observedDisplayStamp = null; } diff --git a/OpenNest.Core/Diagnostics/PlateEdgeReview.cs b/OpenNest.Core/Diagnostics/PlateEdgeReview.cs index efd8013f..b9e1c7b4 100644 --- a/OpenNest.Core/Diagnostics/PlateEdgeReview.cs +++ b/OpenNest.Core/Diagnostics/PlateEdgeReview.cs @@ -19,7 +19,7 @@ namespace OpenNest.Diagnostics; /// table. /// /// -/// Edge spacing: clean nesting material (leads excluded, as the part-spacing check reads it) +/// Outside the work area: clean nesting material (leads excluded, as the part-spacing check reads it) /// outside the plate's work area, or, on an outlined sheet, closer than the largest edge spacing to /// the outline or an obstacle: Auto Nest's layout check judges bounds exactly this way, so a layout /// it accepted stays clear. Shortfalls up to pass. Leads @@ -150,11 +150,11 @@ internal static class PlateEdgeReview bounds.Center); } if (Shortest(bounds, frame.Sheet, frame.Work) is { } close) - return new Verdict(false, $"is {close.Gap:F3} from the plate's {close.Edge} edge, closer than " - + $"its edge spacing ({close.Spacing:F3}).", close.Marker); + return new Verdict(false, $"is outside the work area: {close.Gap:F3} from the plate's {close.Edge} " + + $"edge, inside its {close.Spacing:F3} edge spacing.", close.Marker); if (region != null && frame.Inset > 0 && !frame.Outline.Holds(region, frame.Inset, frame.Origin, Slack)) - return new Verdict(false, $"is closer than the edge spacing ({frame.Inset:F3}) to the plate's outline " - + "or one of its obstacles.", bounds.Center); + return new Verdict(false, $"is outside the work area: inside the {frame.Inset:F3} edge spacing of the " + + "plate's outline or one of its obstacles.", bounds.Center); return null; } diff --git a/OpenNest.Core/Diagnostics/PostVerificationReport.cs b/OpenNest.Core/Diagnostics/PostVerificationReport.cs index e426b8ae..7565f274 100644 --- a/OpenNest.Core/Diagnostics/PostVerificationReport.cs +++ b/OpenNest.Core/Diagnostics/PostVerificationReport.cs @@ -113,7 +113,7 @@ public sealed class PostVerificationReport if (Findings.Any(finding => finding.Kind == PostVerificationKind.OffPlate)) Summary(PostVerificationKind.OffPlate, "Off the plate"); if (Findings.Any(finding => finding.Kind == PostVerificationKind.EdgeSpacing)) - Summary(PostVerificationKind.EdgeSpacing, "Edge spacing"); + Summary(PostVerificationKind.EdgeSpacing, "Outside work area"); if (Findings.Any(finding => finding.Kind == PostVerificationKind.CommonLine)) Summary(PostVerificationKind.CommonLine, "Common lines"); foreach (var finding in Findings) diff --git a/OpenNest.Tests/Diagnostics/OverlapEdgeLabelTests.cs b/OpenNest.Tests/Diagnostics/OverlapEdgeLabelTests.cs index 2a8c1678..167c2509 100644 --- a/OpenNest.Tests/Diagnostics/OverlapEdgeLabelTests.cs +++ b/OpenNest.Tests/Diagnostics/OverlapEdgeLabelTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using OpenNest.Diagnostics; using OpenNest.Geometry; using static OpenNest.Tests.TestHelpers; @@ -12,8 +13,8 @@ public class OverlapEdgeLabelTests [InlineData(118, 10, "Layout: 1 off plate", true)] [InlineData(130, 10, "Layout: 1 off plate", true)] [InlineData(10, -2, "Layout: 1 off plate", true)] - [InlineData(0.2, 10, "Layout: 1 near edge", true)] - public void Summary_ReportsPartsOffThePlateOrNearItsEdge(double x, double y, string expected, bool attention) + [InlineData(0.2, 10, "Layout: 1 outside work area", true)] + public void Summary_ReportsPartsOffThePlateOrOutsideTheWorkArea(double x, double y, string expected, bool attention) { var plate = PlateWith(new Spacing(0.5, 0.5, 0.5, 0.5), Square(x, y)); var state = Publish(plate); @@ -56,6 +57,49 @@ public class OverlapEdgeLabelTests Assert.Equal("Layout: out of date", state.Summary); } + [Fact] + public void EdgeFindings_AreDrawnAndHitAtTheirMarkers() + { + var plate = PlateWith(new Spacing(0.5, 0.5, 0.5, 0.5), Square(10, 10), Square(0.2, 10, "close"), + Square(-1, 30, "hanging")); + var state = Publish(plate); + + Assert.Equal(new[] { (1, false), (2, true) }, + state.DisplayEdgeFindings.Select(finding => (finding.PartId, finding.OffPlate))); + var close = state.DisplayEdgeFindings[0]; + Assert.Equal("2 outside work area", OverlapPairPresentation.Label(close)); + Assert.Equal("3 off plate", OverlapPairPresentation.Label(state.DisplayEdgeFindings[1])); + + var hit = Assert.Single(OverlapPairPresentation.HitTest(state.DisplayEdgeFindings, world => world, + close.Marker + new Vector(3, 0), 96)); + Assert.Same(close, hit); + Assert.Empty(OverlapPairPresentation.HitTest(state.DisplayEdgeFindings, world => world, + close.Marker + new Vector(11, 0), 96)); + var details = OverlapPairPresentation.Details(hit, Units.Inches, CultureInfo.InvariantCulture); + Assert.StartsWith("Outside work area: part 2\n'close' is outside the work area: 0.200 from the plate's " + + "left edge, inside its 0.500 edge spacing.\nMarker: (", details); + } + + [Fact] + public void EdgeMarkers_FollowTheirPartsThroughEdits() + { + var plate = PlateWith(new Spacing(0.5, 0.5, 0.5, 0.5), Square(10, 10), Square(0.2, 10, "close"), + Square(-1, 30, "hanging")); + var state = Publish(plate); + + plate.Parts[0].Offset(1, 0); + state.Invalidate(plate); + Assert.Equal(new[] { 1, 2 }, state.DisplayEdgeFindings.Select(finding => finding.PartId)); + + plate.Parts[2].Offset(5, 0); + state.Invalidate(plate); + Assert.Equal(1, Assert.Single(state.DisplayEdgeFindings).PartId); + + plate.EdgeSpacing = new Spacing(); + state.Invalidate(plate); + Assert.Empty(state.DisplayEdgeFindings); + } + [Fact] public void BareParts_HaveNoSheetToCheck() { diff --git a/OpenNest.Tests/Diagnostics/PlateEdgeReviewTests.cs b/OpenNest.Tests/Diagnostics/PlateEdgeReviewTests.cs index d2a5e7c3..df857ef9 100644 --- a/OpenNest.Tests/Diagnostics/PlateEdgeReviewTests.cs +++ b/OpenNest.Tests/Diagnostics/PlateEdgeReviewTests.cs @@ -15,7 +15,7 @@ public class PlateEdgeReviewTests Assert.Empty(Find(report, PostVerificationKind.OffPlate)); Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing)); Assert.DoesNotContain("Off the plate", report.ToDisplayText()); - Assert.DoesNotContain("Edge spacing", report.ToDisplayText()); + Assert.DoesNotContain("Outside work area", report.ToDisplayText()); } [Fact] @@ -33,10 +33,10 @@ public class PlateEdgeReviewTests var finding = Assert.Single(Find(report, PostVerificationKind.EdgeSpacing)); Assert.Equal((1, 1), (finding.PlateNumber, finding.PartNumber)); - Assert.Contains("0.200 from the plate's left edge", finding.Message); - Assert.Contains("(0.500)", finding.Message); + Assert.Equal("'square' is outside the work area: 0.200 from the plate's left edge, inside its " + + "0.500 edge spacing.", finding.Message); Assert.Equal(0.1, finding.Location!.Value.X, 6); - Assert.Contains("Edge spacing: 1 warning(s)", report.ToDisplayText()); + Assert.Contains("Outside work area: 1 warning(s)", report.ToDisplayText()); Assert.False(report.CanPost(false)); } @@ -157,7 +157,8 @@ public class PlateEdgeReviewTests var report = PostVerificationAnalyzer.Analyze(near); Assert.Empty(Find(report, PostVerificationKind.OffPlate)); var finding = Assert.Single(Find(report, PostVerificationKind.EdgeSpacing)); - Assert.Contains("outline or one of its obstacles", finding.Message); + Assert.Contains("is outside the work area: inside the 0.500 edge spacing of the plate's outline " + + "or one of its obstacles", finding.Message); var clear = NestWith(Edges(0.5), Square(10, 10, 4)); clear.Plates[0].Outline = outline; diff --git a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs index 5fcd0a64..4c4575a2 100644 --- a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs +++ b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs @@ -342,6 +342,38 @@ public class PlateOverlapOverlayTests Assert.Null(run.View.OverlapOverlay.HoverText); }); + [Fact] + public void EdgeMarkersDrawAndHoverWithTheFindingsMessage() => RunSta(() => + { + using var run = new OverlayRun(); + run.View.Plate.Parts.Clear(); + run.View.Plate.EdgeSpacing = new Spacing(0.5, 0, 0, 0); + run.View.Plate.Parts.Add(Rectangle(0.2)); + var hanging = Rectangle(-1); + hanging.Offset(0, 5); + run.View.Plate.Parts.Add(hanging); + run.Finish(); + Assert.Equal("Layout: 1 off plate, 1 outside work area", run.View.OverlapSummary); + run.View.ZoomToPoint(new Vector(), 20); + run.View.SetOrigin(60, 220); + using var image = new Bitmap(300, 300); + using var graphics = Graphics.FromImage(image); + graphics.Clear(Color.White); + graphics.TranslateTransform(60, 220); + run.View.OverlapOverlay.Draw(graphics); + + var radius = (int)OverlapPairPresentation.MarkerHalfSize(run.View.DeviceDpi); + foreach (var finding in run.View.OverlapReport.EdgeFindings) + { + var center = run.View.PointWorldToControl(finding.Marker); + Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(center.X - radius, center.Y).ToArgb()); + run.View.MoveTo(center); + Assert.Contains(finding.Message, run.View.OverlapOverlay.HoverText); + Assert.StartsWith(finding.OffPlate ? "Off the plate: part 2" : "Outside work area: part 1", + run.View.OverlapOverlay.HoverText); + } + }); + [Fact] public void ClearPlateDrawsNoLabelButStatusKeepsTheWords() => RunSta(() => { diff --git a/OpenNest/Controls/OverlapOverlayController.cs b/OpenNest/Controls/OverlapOverlayController.cs index f8e325f3..acd8cde1 100644 --- a/OpenNest/Controls/OverlapOverlayController.cs +++ b/OpenNest/Controls/OverlapOverlayController.cs @@ -40,6 +40,7 @@ internal sealed class OverlapOverlayController : IDisposable private Plate observedPlate; private IReadOnlyList hoveredPairs = Array.Empty(); private IReadOnlyList hoveredSpacing = Array.Empty(); + private IReadOnlyList hoveredEdges = Array.Empty(); private string hoverText; private OverlapHoverPages hoverPages; private Point hoverPoint; @@ -409,6 +410,7 @@ internal sealed class OverlapOverlayController : IDisposable { DrawCutOffClampContacts(graphics); DrawSpacingMarkers(graphics); + DrawEdgeMarkers(graphics); } DrawStateLabel(graphics); } @@ -464,24 +466,28 @@ internal sealed class OverlapOverlayController : IDisposable var graph = view.PointWorldToGraph(world); return new Vector(graph.X, graph.Y); } - // Overlap markers exist only in these modes; spacing markers in every visible one. + // Overlap markers exist only in these modes; spacing and edge markers in every visible one. var hits = ShowsCentroids ? OverlapPairPresentation.HitTest(state.DisplayPairs, ToGraph, pointer, view.DeviceDpi) : Array.Empty(); var spacingHits = OverlapPairPresentation.HitTest(state.DisplaySpacingPairs, ToGraph, pointer, view.DeviceDpi); - var changed = !hits.SequenceEqual(hoveredPairs) || !spacingHits.SequenceEqual(hoveredSpacing); + var edgeHits = OverlapPairPresentation.HitTest(state.DisplayEdgeFindings, ToGraph, pointer, view.DeviceDpi); + var changed = !hits.SequenceEqual(hoveredPairs) || !spacingHits.SequenceEqual(hoveredSpacing) + || !edgeHits.SequenceEqual(hoveredEdges); var moved = point != hoverPoint; hoveredPairs = hits; hoveredSpacing = spacingHits; + hoveredEdges = edgeHits; hoverPoint = point; hoverTransform = ViewTransform; if (changed) { hoverPages = null; HoverBounds = RectangleF.Empty; - hoverText = hits.Count + spacingHits.Count == 0 ? null : string.Join("\n\n", + hoverText = hits.Count + spacingHits.Count + edgeHits.Count == 0 ? null : string.Join("\n\n", hits.Select(pair => OverlapPairPresentation.Details(pair, capturedUnits)) - .Concat(spacingHits.Select(pair => OverlapPairPresentation.Details(pair, capturedUnits)))); + .Concat(spacingHits.Select(pair => OverlapPairPresentation.Details(pair, capturedUnits))) + .Concat(edgeHits.Select(finding => OverlapPairPresentation.Details(finding, capturedUnits)))); } if (changed || (moved && hoverText != null)) view.Invalidate(); @@ -501,6 +507,7 @@ internal sealed class OverlapOverlayController : IDisposable HoverBounds = RectangleF.Empty; hoveredPairs = Array.Empty(); hoveredSpacing = Array.Empty(); + hoveredEdges = Array.Empty(); if (visible && !disposed && !view.IsDisposed && !view.Disposing) view.Invalidate(); } @@ -564,7 +571,7 @@ internal sealed class OverlapOverlayController : IDisposable using var format = (StringFormat)StringFormat.GenericTypographic.Clone(); format.FormatFlags |= StringFormatFlags.NoWrap | StringFormatFlags.MeasureTrailingSpaces; float Measure(string line) => graphics.MeasureString(line, view.Font, PointF.Empty, format).Width; - hoverPages ??= OverlapHoverPages.Create(text, hoveredPairs.Count + hoveredSpacing.Count, maxRows, maxWidth, line => Measure(line)); + hoverPages ??= OverlapHoverPages.Create(text, hoveredPairs.Count + hoveredSpacing.Count + hoveredEdges.Count, maxRows, maxWidth, line => Measure(line)); if (hoverPages.NeedsLargerViewport) { // A viewport smaller than a content line plus its paging hint cannot @@ -703,6 +710,47 @@ internal sealed class OverlapOverlayController : IDisposable } } + // Edge findings mark a point, not shared area, so every visible mode shows them: off the + // plate in the overlaps' red crosshair, outside the work area in the spacing ring's orange. + private void DrawEdgeMarkers(Graphics graphics) + { + var dpiScale = view.DeviceDpi / 96f; + var radius = (float)OverlapPairPresentation.MarkerHalfSize(view.DeviceDpi); + var spacingColor = Color.FromArgb(200, 90, 0); + using var halo = new Pen(Color.White, 4 * dpiScale); + using var crosshair = new Pen(Color.DarkRed, 2 * dpiScale); + using var ring = new Pen(spacingColor, 2 * dpiScale); + using var spacingText = new SolidBrush(spacingColor); + var labelRows = new Dictionary(); + foreach (var finding in state.DisplayEdgeFindings) + { + var center = view.PointWorldToGraph(finding.Marker); + if (!float.IsFinite(center.X) || !float.IsFinite(center.Y)) + continue; + if (finding.OffPlate) + { + foreach (var pen in new[] { halo, crosshair }) + { + graphics.DrawLine(pen, center.X - radius, center.Y, center.X + radius, center.Y); + graphics.DrawLine(pen, center.X, center.Y - radius, center.X, center.Y + radius); + } + } + else + { + foreach (var pen in new[] { halo, ring }) + graphics.DrawEllipse(pen, center.X - radius, center.Y - radius, radius * 2, radius * 2); + } + var label = OverlapPairPresentation.Label(finding); + var size = graphics.MeasureString(label, view.Font); + labelRows.TryGetValue(center, out var row); + labelRows[center] = row + 1; + var x = center.X + radius + 3 * dpiScale; + var y = center.Y - radius + row * size.Height; + graphics.FillRectangle(Brushes.White, x, y, size.Width, size.Height); + graphics.DrawString(label, view.Font, finding.OffPlate ? Brushes.DarkRed : spacingText, x, y); + } + } + private void DrawStateLabel(Graphics graphics) { var saved = graphics.Save(); diff --git a/OpenNest/Forms/CuttingInspectionPanel.cs b/OpenNest/Forms/CuttingInspectionPanel.cs index 2c8a55c4..319e2785 100644 --- a/OpenNest/Forms/CuttingInspectionPanel.cs +++ b/OpenNest/Forms/CuttingInspectionPanel.cs @@ -643,7 +643,7 @@ internal sealed class CuttingInspectionPanel : UserControl PostVerificationKind.PartSpacing => "Part spacing", PostVerificationKind.PartSupport => "Part support", PostVerificationKind.OffPlate => "Off the plate", - PostVerificationKind.EdgeSpacing => "Edge spacing", + PostVerificationKind.EdgeSpacing => "Outside work area", PostVerificationKind.CommonLine => "Common line", _ => kind.ToString(), }; @@ -712,7 +712,7 @@ internal sealed class CuttingInspectionPanel : UserControl PostVerificationKind.PartSpacing => "The ring marks where the two parts come closest.", PostVerificationKind.PartSupport => "The ring marks the part's center of gravity.", PostVerificationKind.OffPlate => "The ring marks where the part leaves the plate.", - PostVerificationKind.EdgeSpacing => "The ring marks the gap between the part and the plate edge.", + PostVerificationKind.EdgeSpacing => "The ring marks where the part leaves the work area, inside the edge spacing.", PostVerificationKind.CommonLine => "The ring marks the common-line part or lead to review.", _ => "The ring marks the affected part; the exact spot is not known." }; diff --git a/OpenNest/Forms/PostVerificationForm.cs b/OpenNest/Forms/PostVerificationForm.cs index b0f30b7e..cde7bf0d 100644 --- a/OpenNest/Forms/PostVerificationForm.cs +++ b/OpenNest/Forms/PostVerificationForm.cs @@ -29,7 +29,7 @@ public partial class PostVerificationForm : Form (PostVerificationKind.FixedLead, "Fixed leads"), (PostVerificationKind.PierceZone, "Pierce zones"), (PostVerificationKind.PartSpacing, "Part spacing"), - (PostVerificationKind.EdgeSpacing, "Edge spacing"), + (PostVerificationKind.EdgeSpacing, "Outside work area"), (PostVerificationKind.PartSupport, "Part support"), (PostVerificationKind.CommonLine, "Common lines"), }; diff --git a/docs/geometry/visual-overlap-check.md b/docs/geometry/visual-overlap-check.md index c01bf276..6a1d7df6 100644 --- a/docs/geometry/visual-overlap-check.md +++ b/docs/geometry/visual-overlap-check.md @@ -58,9 +58,14 @@ the recheck. Changing the plate's part spacing clears them and reruns the check. The label also checks the plate edge with the pre-post check's `PlateEdgeReview`, on clean material only (the snapshot holds no leads): a part past the sheet, or outside an outlined sheet or over one of its obstacles, counts as **off plate**, and one inside the edge spacing -as **near edge**, as in **Layout: 1 overlap, 1 off plate**. A lead or scribe past the sheet -is left to Validate Nest and posting, which read the placed programs. These findings have no -canvas marker. Changing the plate's edge spacing or outline makes the result out of date. +as **outside work area**, as in **Layout: 1 overlap, 1 off plate, 1 outside work area**. A +lead or scribe past the sheet is left to Validate Nest and posting, which read the placed +programs. Each finding is marked in every display mode except Off: off the plate with a red +crosshair where the part leaves the sheet, outside the work area with an orange ring at the +middle of its gap to the edge, labeled like `3 off plate` or `2 outside work area`. Hover near +a marker for the finding's message and marker coordinates. Markers follow the same retention +as overlap highlights: only a changed part's marker disappears before the recheck. Changing +the plate's size, edge spacing or outline clears them and makes the result out of date. In a nest window the active plate is checked automatically. Any layout edit (add, remove, reorder, move, rotate, fill) invalidates the full report and shows diff --git a/docs/post-verification.md b/docs/post-verification.md index f902d1c1..1b25cb7b 100644 --- a/docs/post-verification.md +++ b/docs/post-verification.md @@ -7,8 +7,8 @@ planner or selecting a post processor. It opens the editor's read-only side pane in **Validate Nest (live programs)** mode, distinct from the panel's unapplied **Plan Cutting** proposal. The validation panel checks overlapping material, missing lead-ins, rapid crossings in the existing placed programs and cutting -order, parts closer than the plate's part spacing, and parts off the plate or inside -its edge spacing. Findings identify plate and part +order, parts closer than the plate's part spacing, and parts off the plate or outside +its work area (inside the edge spacing). Findings identify plate and part sequence numbers in a read-only grid; incomplete or failed checks are never reported as clear. Select a row to switch the main plate view to that plate and focus the finding. A colored headline gives @@ -24,8 +24,8 @@ Overlaps use the same magenta material shading as **View > Overlap Check**. A DPI-scaled ring expands and fades continuously while the finding is selected; both its color and contrasting halo fade as it grows. For overlaps the ring marks a point inside the largest shared patch; for missing lead-ins it marks the contour start; for part spacing it marks -the middle of the closest gap; for edge spacing it marks the gap between the part and -the edge; for a part off the plate it marks where it leaves the plate. Rapid warnings +the middle of the closest gap; for a part outside the work area it marks the gap between +the part and the edge; for a part off the plate it marks where it leaves the plate. Rapid warnings pulse the offending orange dashed segment continuously and mark its contacts with the completed contour using fixed-screen-size magenta crosshairs with white halos. Contacts use native lines/arcs and the rapid check's existing numerical tolerances; @@ -130,12 +130,14 @@ shows the first three check categories, even when there are no findings: whose clean material lies outside the outline or over one of its obstacles. The finding gives how far it runs past which edge. This line appears in the summary only when there is such a finding. -- **Edge spacing:** a part on the sheet whose clean nesting material (leads excluded) lies +- **Outside work area:** a part on the sheet whose clean nesting material (leads excluded) lies outside the plate's work area, so closer to an edge than that side's edge spacing, or, on an outlined sheet, closer than the largest edge spacing to the outline or an obstacle. This is the bound Auto Nest's layout check applies, so a layout it accepted stays clear; shortfalls up to `SpacingSlack` pass. Leads may run into the edge spacing. The finding - gives the gap and the side's spacing. Zero edge spacing allows contact with the edge. + says the part is outside the work area and gives the gap and the side's spacing, as in + `'name' is outside the work area: 0.200 from the plate's left edge, inside its 0.500 edge + spacing.` (`PostVerificationKind.EdgeSpacing`). Zero edge spacing allows contact with the edge. Cut-off parts are exempt from both checks. This line appears in the summary only when there is such a finding. - **Nested parts:** a part whose perimeter lies strictly inside a cutout of another part (its