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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-06 12:28:52 -05:00
1 parent 7cc0e6f126
commit 4aa035af2f
13 files changed
+198 -33

No files matched your search

@@ -95,6 +95,11 @@ public sealed class OverlapGeometryStamp
ReferenceEquals(plate, current) && partSpacing == Bits(current.PartSpacing)
&& commonLines == CommonLineState(current);
/// <summary>The same sheet, edge spacing and outline: an unchanged part keeps its edge finding.</summary>
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) + ":"
@@ -37,6 +37,18 @@ public static class OverlapPairPresentation
+ $"Marker: ({Number(pair.Marker.X, provider)}, {Number(pair.Marker.Y, provider)}) {units}";
}
/// <summary>A short canvas label: the part's sequence number and what is wrong.</summary>
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();
/// <inheritdoc cref="HitTest(IEnumerable{PlateOverlapPair}, Func{Vector, Vector}, Vector, int)"/>
public static IReadOnlyList<PlateEdgeFinding> HitTest(IEnumerable<PlateEdgeFinding> findings,
Func<Vector, Vector> 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);
@@ -26,6 +26,9 @@ public sealed class OverlapReportState
/// <summary>Known part-spacing pairs safe to draw, retained like <see cref="DisplayPairs"/>.</summary>
public IReadOnlyList<PlateSpacingPair> DisplaySpacingPairs { get; private set; } =
Array.Empty<PlateSpacingPair>();
/// <summary>Known plate-edge findings safe to draw, retained like <see cref="DisplayPairs"/>.</summary>
public IReadOnlyList<PlateEdgeFinding> DisplayEdgeFindings { get; private set; } =
Array.Empty<PlateEdgeFinding>();
public bool IsRunning => Status == OverlapCheckStatus.Checking;
/// <summary>A current, fully checked report with no finding of any kind.</summary>
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<PlateOverlapPair>();
DisplayCutOffClampFindings = Array.Empty<CutOffClampFinding>();
DisplaySpacingPairs = Array.Empty<PlateSpacingPair>();
DisplayEdgeFindings = Array.Empty<PlateEdgeFinding>();
displayStamp = null;
observedDisplayStamp = null;
}
+5 -5
View File
@@ -19,7 +19,7 @@ namespace OpenNest.Diagnostics;
/// table.
/// </para>
/// <para>
/// <b>Edge spacing</b>: clean nesting material (leads excluded, as the part-spacing check reads it)
/// <b>Outside the work area</b>: 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 <see cref="SpacingOutline.SpacingSlack"/> 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;
}
@@ -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)
@@ -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()
{
@@ -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;
@@ -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(() =>
{
+53 -5
View File
@@ -40,6 +40,7 @@ internal sealed class OverlapOverlayController : IDisposable
private Plate observedPlate;
private IReadOnlyList<PlateOverlapPair> hoveredPairs = Array.Empty<PlateOverlapPair>();
private IReadOnlyList<PlateSpacingPair> hoveredSpacing = Array.Empty<PlateSpacingPair>();
private IReadOnlyList<PlateEdgeFinding> hoveredEdges = Array.Empty<PlateEdgeFinding>();
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<PlateOverlapPair>();
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<PlateOverlapPair>();
hoveredSpacing = Array.Empty<PlateSpacingPair>();
hoveredEdges = Array.Empty<PlateEdgeFinding>();
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<PointF, int>();
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();
+2 -2
View File
@@ -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."
};
+1 -1
View File
@@ -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"),
};
+8 -3
View File
@@ -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
+8 -6
View File
@@ -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