feat(verify): warn when parts leave the plate or its edge spacing

Validate Nest and the pre-post review checked overlaps and part-to-part
spacing but never the sheet itself, so a part dragged past the plate edge,
a lead-in piercing off the sheet, or a part inside the edge spacing could
post without a warning. Only Auto Nest's own layout check held parts to
the work area.

PlateEdgeReview adds two finding kinds:
- OffPlate: any cut, lead or scribe motion past the sheet, or clean
  material outside an outlined sheet or over one of its obstacles.
- EdgeSpacing: clean material (leads excluded) outside the work area, or
  closer than the largest edge spacing to an outline or obstacle - the
  bound Auto Nest's layout check applies, with SpacingSlack, so layouts it
  accepted stay clear.

Cut-off parts are exempt. Routing-replay test fixtures sit at the sheet
origin with leads hanging off it; their route assertions now ignore the
sheet-bound kinds, and the post-contract fixture moves onto the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-05 08:59:23 -05:00
1 parent 3cd7729d38
commit f9abab0c6d
14 files changed
+459 -13

No files matched your search

@@ -21,7 +21,7 @@ namespace OpenNest.Diagnostics;
/// is short of the spacing by more than the slack. Zero spacing enforces nothing, so contact stays
/// valid.
/// Pairs whose material already overlaps, and parts the overlap check could not read, are left to
/// that check. The sheet edge and clamps are not spacing-checked here. The pre-post review and the
/// that check. Clamps are not spacing-checked here; <see cref="PlateEdgeReview"/> checks the sheet edge. The pre-post review and the
/// desktop overlap label share this scan, so they cannot disagree.
/// </summary>
internal static class PartSpacingReview
@@ -172,7 +172,7 @@ internal static class PartSpacingReview
/// <summary>Clean drawing material, rotated to the part's pose; null when the part has none.
/// Converted once per program and rotation.</summary>
private static ShapeProfile Profile(Part part, Dictionary<(Program, long), ShapeProfile> profiles)
internal static ShapeProfile Profile(Part part, Dictionary<(Program, long), ShapeProfile> profiles)
{
var program = part.BaseDrawing.Program;
var rotation = part.Rotation - program.Rotation;
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Clipper2Lib;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Diagnostics;
/// <summary>
/// Parts that leave the sheet or come closer to its edges than the plate's edge spacing.
/// <para>
/// <b>Off the plate</b>: any cutting, lead or scribe motion of the placed program (its native
/// curve bounds) past the sheet's bounding box, or, on an outlined sheet, clean material outside
/// the outline or over one of its obstacles. A pierce or cut off the sheet cuts nothing, or the
/// table.
/// </para>
/// <para>
/// <b>Edge spacing</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
/// may run into the edge spacing; only off the sheet do they warn.
/// </para>
/// Cut-off parts are exempt: they run to the sheet edge by design.
/// </summary>
internal static class PlateEdgeReview
{
private const double Slack = SpacingOutline.SpacingSlack;
internal static void Add(Plate plate, int plateNumber, List<PostVerificationFinding> findings,
CancellationToken cancellationToken)
{
if (plate.Parts.Count == 0)
return;
var size = plate.Size;
if (!(size.Length > 0) || !(size.Width > 0) || !double.IsFinite(size.Length) || !double.IsFinite(size.Width))
{
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, null, null,
$"Plate edge check: plate size {size} is not a positive, finite size."));
return;
}
var edges = plate.EdgeSpacing;
if (!new[] { edges.Left, edges.Bottom, edges.Right, edges.Top }.All(e => double.IsFinite(e) && e >= 0))
{
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, null, null,
"Plate edge check: plate edge spacing is not finite and non-negative on every side."));
return;
}
var sheet = plate.BoundingBox(includeParts: false);
var work = plate.WorkArea();
var outline = plate.Outline;
var origin = new Vector(sheet.X, sheet.Y);
var inset = System.Math.Max(System.Math.Max(edges.Left, edges.Right), System.Math.Max(edges.Top, edges.Bottom));
var profiles = new Dictionary<(Program, long), ShapeProfile>();
for (var index = 0; index < plate.Parts.Count; index++)
{
cancellationToken.ThrowIfCancellationRequested();
var part = plate.Parts[index];
var partNumber = index + 1;
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff)
continue;
try
{
if (PastSheet(part, sheet, cancellationToken) is { } past)
{
findings.Add(new(PostVerificationKind.OffPlate, plateNumber, partNumber, null,
$"'{part.BaseDrawing.Name}' runs {past.Distance:F3} past the plate's {past.Edge} edge.")
{ Location = past.Marker });
continue;
}
var profile = PartSpacingReview.Profile(part, profiles);
if (profile == null)
continue;
PathsD region = null;
if (outline != null)
{
var material = SpacingOutline.Build(profile, part.Location, 0)
?? throw new ArgumentException("Part material has no closed perimeter.");
region = new PathsD { ClipperBridge.ToPath(material.Perimeter, positive: true) };
if (!outline.Holds(region, 0, origin, Slack))
{
findings.Add(new(PostVerificationKind.OffPlate, plateNumber, partNumber, null,
$"'{part.BaseDrawing.Name}' lies outside the plate's outline or over one of its obstacles.")
{ Location = Center(profile, part.Location) });
continue;
}
}
var bounds = profile.Perimeter.BoundingBox.Translate(part.Location);
if (Shortest(bounds, sheet, work) is { } close)
{
findings.Add(new(PostVerificationKind.EdgeSpacing, plateNumber, partNumber, null,
$"'{part.BaseDrawing.Name}' is {close.Gap:F3} from the plate's {close.Edge} edge, closer than "
+ $"its edge spacing ({close.Spacing:F3}).")
{ Location = close.Marker });
continue;
}
if (region != null && inset > 0 && !outline.Holds(region, inset, origin, Slack))
findings.Add(new(PostVerificationKind.EdgeSpacing, plateNumber, partNumber, null,
$"'{part.BaseDrawing.Name}' is closer than the edge spacing ({inset:F3}) to the plate's outline "
+ "or one of its obstacles.")
{ Location = Center(profile, part.Location) });
}
catch (Exception exception) when (IsInvalid(exception))
{
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null,
"Plate edge check: " + exception.Message));
}
}
}
private readonly record struct Past(string Edge, double Distance, Vector Marker);
private readonly record struct Close(string Edge, double Gap, double Spacing, Vector Marker);
/// <summary>The furthest any non-rapid motion of the placed program runs past the sheet, or null.</summary>
private static Past? PastSheet(Part part, Box sheet, CancellationToken cancellationToken)
{
Past? worst = null;
foreach (var move in ExecutionMotionReader.Read(part.Program, part.Location, null, cancellationToken).Motions)
{
if (move.Rapid || move.Curve == null)
continue;
var box = move.Curve.ToEntity().BoundingBox;
var middle = move.Curve.Midpoint;
Consider("left", sheet.Left - box.Left, new Vector(sheet.Left, Clamp(middle.Y, sheet.Bottom, sheet.Top)));
Consider("bottom", sheet.Bottom - box.Bottom, new Vector(Clamp(middle.X, sheet.Left, sheet.Right), sheet.Bottom));
Consider("right", box.Right - sheet.Right, new Vector(sheet.Right, Clamp(middle.Y, sheet.Bottom, sheet.Top)));
Consider("top", box.Top - sheet.Top, new Vector(Clamp(middle.X, sheet.Left, sheet.Right), sheet.Top));
}
return worst;
void Consider(string edge, double distance, Vector marker)
{
if (!double.IsFinite(distance))
throw new ArgumentException("Placed program geometry must be finite.");
if (distance > Slack && (worst == null || distance > worst.Value.Distance))
worst = new Past(edge, distance, marker);
}
}
/// <summary>The side whose edge spacing the material falls furthest short of, or null.</summary>
private static Close? Shortest(Box bounds, Box sheet, Box work)
{
Close? worst = null;
var shortfall = 0.0;
var center = bounds.Center;
Consider("left", work.Left - bounds.Left, bounds.Left - sheet.Left, work.Left - sheet.Left,
new Vector((sheet.Left + bounds.Left) / 2, center.Y));
Consider("bottom", work.Bottom - bounds.Bottom, bounds.Bottom - sheet.Bottom, work.Bottom - sheet.Bottom,
new Vector(center.X, (sheet.Bottom + bounds.Bottom) / 2));
Consider("right", bounds.Right - work.Right, sheet.Right - bounds.Right, sheet.Right - work.Right,
new Vector((sheet.Right + bounds.Right) / 2, center.Y));
Consider("top", bounds.Top - work.Top, sheet.Top - bounds.Top, sheet.Top - work.Top,
new Vector(center.X, (sheet.Top + bounds.Top) / 2));
return worst;
void Consider(string edge, double by, double gap, double spacing, Vector marker)
{
if (by > Slack && by > shortfall)
{
shortfall = by;
worst = new Close(edge, System.Math.Max(0, gap), spacing, marker);
}
}
}
private static Vector Center(ShapeProfile profile, Vector location) =>
profile.Perimeter.BoundingBox.Translate(location).Center;
private static double Clamp(double value, double low, double high) =>
double.IsFinite(value) ? System.Math.Clamp(value, low, high) : low;
private static bool IsInvalid(Exception exception) => exception is
ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException;
}
@@ -226,6 +226,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 must stay on the sheet and keep its edge spacing.
PlateEdgeReview.Add(plate, plateNumber, findings, cancellationToken);
// Parts the support bed cannot hold level need a tab that keeps them level.
PartSupportReview.Add(plate, plateNumber, findings, cancellationToken);
}
@@ -40,7 +40,19 @@ public enum PostVerificationKind
/// 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).
/// </summary>
PartSupport
PartSupport,
/// <summary>
/// A part's cuts, leads or scribes run past the sheet, or its material lies outside an outlined
/// sheet or over one of its obstacles.
/// </summary>
OffPlate,
/// <summary>
/// A part's clean material is closer to the sheet's edge than the plate's edge spacing, judged as
/// Auto Nest's layout check judges it.
/// </summary>
EdgeSpacing
}
/// <summary>Plate and part numbers are one-based; plate zero denotes a whole-post limitation.</summary>
@@ -92,6 +104,10 @@ public sealed class PostVerificationReport
Summary(PostVerificationKind.PartSpacing, "Part spacing");
if (Findings.Any(finding => finding.Kind == PostVerificationKind.PartSupport))
Summary(PostVerificationKind.PartSupport, "Part support");
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");
foreach (var finding in Findings)
{
text.Append(finding.PlateNumber == 0 ? "Post processor" : $"Plate {finding.PlateNumber}");
@@ -80,7 +80,7 @@ public class CuttingPlanCommitTests
var detached = proposal.CopyProgram();
detached.Codes.Clear();
Assert.NotEmpty(part.Program.Codes);
Assert.Empty(PostVerificationAnalyzer.Analyze(nest, Vector.Zero).Findings);
Assert.Empty(TestHelpers.RouteFindings(PostVerificationAnalyzer.Analyze(nest, Vector.Zero)).Findings);
}
[Theory]
@@ -411,6 +411,6 @@ public class CuttingPlanServiceTests
Assert.True(copy.RestoreLeadInProgram((Program)source.Program.Clone(), source.LeadInsLocked));
plate.Parts.Add(copy);
}
return PostVerificationAnalyzer.Analyze(nest, start);
return TestHelpers.RouteFindings(PostVerificationAnalyzer.Analyze(nest, start));
}
}
@@ -79,7 +79,7 @@ public class JointCuttingPlanTests
var copy = new Part(new Drawing("replay", (Program)part.BaseDrawing.Program.Clone()), proposal.Location);
Assert.True(copy.RestoreLeadInProgram(proposal.CopyProgram(), false));
plate.Parts.Add(copy);
var report = PostVerificationAnalyzer.Analyze(nest, Vector.Zero);
var report = TestHelpers.RouteFindings(PostVerificationAnalyzer.Analyze(nest, Vector.Zero));
Assert.Empty(report.Findings);
var detached = proposal.CopyProgram(); detached.Codes.Clear(); detached.SubPrograms.Clear();
Assert.NotEmpty(proposal.CopyProgram().Codes);
@@ -232,7 +232,7 @@ public class ReorderSearchTests
Assert.True(copy.RestoreLeadInProgram((Program)source.Program.Clone(), source.LeadInsLocked));
plate.Parts.Add(copy);
}
return PostVerificationAnalyzer.Analyze(nest, start);
return TestHelpers.RouteFindings(PostVerificationAnalyzer.Analyze(nest, start));
}
private static string Describe(CuttingPlanResult r) =>
@@ -0,0 +1,216 @@
using OpenNest.CNC;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
using static OpenNest.Tests.TestHelpers;
namespace OpenNest.Tests.Diagnostics;
public class PlateEdgeReviewTests
{
[Fact]
public void PartInsideWorkArea_IsClear()
{
var report = Analyze(Edges(0.5), Square(1, 1, 4));
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());
}
[Fact]
public void PartAtEdgeSpacing_IsClear()
{
var report = Analyze(Edges(0.5), Square(0.5, 0.5, 4));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void PartInsideEdgeSpacing_WarnsWithGapAndSide()
{
var report = Analyze(Edges(0.5), Square(0.2, 2, 4));
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(0.1, finding.Location!.Value.X, 6);
Assert.Contains("Edge spacing: 1 warning(s)", report.ToDisplayText());
Assert.False(report.CanPost(false));
}
[Fact]
public void FarEdges_FollowThePlateSize()
{
// Default plate is 120 long (X) and 60 wide (Y).
var finding = Assert.Single(Find(Analyze(Edges(0.5), Square(115.7, 20, 4)),
PostVerificationKind.EdgeSpacing));
Assert.Contains("right edge", finding.Message);
finding = Assert.Single(Find(Analyze(Edges(0.5), Square(20, 55.9, 4)), PostVerificationKind.EdgeSpacing));
Assert.Contains("top edge", finding.Message);
}
[Fact]
public void ShortfallWithinSlack_IsClear()
{
var report = Analyze(Edges(0.5), Square(0.5 - SpacingOutline.SpacingSlack / 2, 2, 4));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void PartPastSheet_IsOffPlateNotEdgeSpacing()
{
var report = Analyze(Edges(0.5), Square(-1, 2, 4));
var finding = Assert.Single(Find(report, PostVerificationKind.OffPlate));
Assert.Contains("1.000 past the plate's left edge", finding.Message);
Assert.Equal(0, finding.Location!.Value.X, 6);
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
Assert.Contains("Off the plate: 1 warning(s)", report.ToDisplayText());
}
[Fact]
public void PartEntirelyOffSheet_IsOffPlate()
{
var finding = Assert.Single(Find(Analyze(new Spacing(), Square(130, 10, 4)), PostVerificationKind.OffPlate));
Assert.Contains("past the plate's right edge", finding.Message);
}
[Fact]
public void ZeroEdgeSpacing_AllowsContactWithTheEdge()
{
var report = Analyze(new Spacing(), Square(0, 0, 4), Square(116, 56, 4));
Assert.Empty(Find(report, PostVerificationKind.OffPlate));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void LeadInPastSheet_IsOffPlate()
{
// The part keeps zero edge spacing, but its lead-in starts 0.25 off the left edge.
var program = new Program();
program.MoveTo(-0.25, 2);
program.Codes.Add(new LinearMove(0, 2) { Layer = LayerType.Leadin });
program.LineTo(0, 0);
program.LineTo(4, 0);
program.LineTo(4, 4);
program.LineTo(0, 4);
program.LineTo(0, 2);
var part = new Part(new Drawing("leaded", program), new Vector(0, 0));
var report = Analyze(new Spacing(), part);
var finding = Assert.Single(Find(report, PostVerificationKind.OffPlate));
Assert.Contains("0.250 past the plate's left edge", finding.Message);
}
[Fact]
public void LeadInsideEdgeSpacing_IsClear()
{
// Leads may run into the edge spacing; only material is held to it.
var program = new Program();
program.MoveTo(-0.25, 2);
program.Codes.Add(new LinearMove(0, 2) { Layer = LayerType.Leadin });
program.LineTo(0, 0);
program.LineTo(4, 0);
program.LineTo(4, 4);
program.LineTo(0, 4);
program.LineTo(0, 2);
var part = new Part(new Drawing("leaded", program), new Vector(0.5, 0.5));
var report = Analyze(Edges(0.5), part);
Assert.Empty(Find(report, PostVerificationKind.OffPlate));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void Quadrant3Sheet_ChecksItsSignedFrame()
{
var nest = new Nest();
nest.PlateDefaults.Quadrant = 3;
var plate = nest.CreatePlate();
plate.EdgeSpacing = Edges(0.5);
plate.Parts.Add(Square(-4.2, -10, 4));
var finding = Assert.Single(Find(PostVerificationAnalyzer.Analyze(nest), PostVerificationKind.EdgeSpacing));
Assert.Contains("right edge", finding.Message);
}
[Fact]
public void OutlinedSheet_WarnsOverObstacleAndNearOutline()
{
var outline = new SheetOutline(
new[] { new Vector(0, 0), new Vector(120, 0), new Vector(120, 60), new Vector(0, 60) },
new[] { new[] { new Vector(50, 20), new Vector(70, 20), new Vector(70, 40), new Vector(50, 40) } });
var over = NestWith(Edges(0.5), Square(48, 25, 4));
over.Plates[0].Outline = outline;
Assert.Single(Find(PostVerificationAnalyzer.Analyze(over), PostVerificationKind.OffPlate));
var near = NestWith(Edges(0.5), Square(45.8, 25, 4));
near.Plates[0].Outline = outline;
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);
var clear = NestWith(Edges(0.5), Square(10, 10, 4));
clear.Plates[0].Outline = outline;
report = PostVerificationAnalyzer.Analyze(clear);
Assert.Empty(Find(report, PostVerificationKind.OffPlate));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void CutOffParts_AreExempt()
{
var drawing = new Drawing("cutoff", Line(-1, 30, 125, 30)) { IsCutOff = true };
var report = Analyze(Edges(0.5), new Part(drawing, new Vector(0, 0)));
Assert.Empty(Find(report, PostVerificationKind.OffPlate));
Assert.Empty(Find(report, PostVerificationKind.EdgeSpacing));
}
[Fact]
public void InvalidEdgeSpacing_IsIncomplete()
{
var report = Analyze(new Spacing(double.NaN, 0, 0, 0), Square(10, 10, 4));
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.Incomplete
&& f.Message.StartsWith("Plate edge check:"));
}
private static Program Line(double x1, double y1, double x2, double y2)
{
var program = new Program();
program.MoveTo(x1, y1);
program.LineTo(x2, y2);
return program;
}
private static Spacing Edges(double all) => new(all, all, all, all);
private static PostVerificationFinding[] Find(PostVerificationReport report, PostVerificationKind kind) =>
report.Findings.Where(f => f.Kind == kind).ToArray();
private static PostVerificationReport Analyze(Spacing edges, params Part[] parts) =>
PostVerificationAnalyzer.Analyze(NestWith(edges, parts));
private static Nest NestWith(Spacing edges, params Part[] parts)
{
var nest = new Nest();
var plate = nest.CreatePlate();
plate.EdgeSpacing = edges;
foreach (var part in parts)
plate.Parts.Add(part);
return nest;
}
private static Part Square(double x, double y, double size, string name = "square") =>
new(new Drawing(name, MakeSquareProgram(0, 0, size)), new Vector(x, y));
}
@@ -53,7 +53,7 @@ public class PostVerificationPostContractTests
program.LineTo(4, 4);
program.LineTo(0, 4);
program.LineTo(0, 0);
var part = new Part(new Drawing("square", program));
var part = new Part(new Drawing("square", program), new Vector(5, 5));
part.ApplyLeadIns(new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5 } },
new Vector(-2, -2));
var nest = new Nest();
+9
View File
@@ -34,6 +34,15 @@ internal static class TestConfig
internal static class TestHelpers
{
/// <summary>
/// The report without its sheet-bound findings, for routing replays whose fixtures sit at the
/// sheet origin with leads hanging off it. PlateEdgeReviewTests covers those checks.
/// </summary>
public static OpenNest.Diagnostics.PostVerificationReport RouteFindings(
OpenNest.Diagnostics.PostVerificationReport report) =>
new(report.Findings.Where(f => f.Kind is not (OpenNest.Diagnostics.PostVerificationKind.OffPlate
or OpenNest.Diagnostics.PostVerificationKind.EdgeSpacing)));
public static Part MakePartAt(double x, double y, double size = 1)
{
// CW winding matches CNC convention (OffsetSide.Left = outward)
+4
View File
@@ -642,6 +642,8 @@ internal sealed class CuttingInspectionPanel : UserControl
PostVerificationKind.PierceZone => "Pierce zone",
PostVerificationKind.PartSpacing => "Part spacing",
PostVerificationKind.PartSupport => "Part support",
PostVerificationKind.OffPlate => "Off the plate",
PostVerificationKind.EdgeSpacing => "Edge spacing",
_ => kind.ToString(),
};
@@ -708,6 +710,8 @@ internal sealed class CuttingInspectionPanel : UserControl
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.",
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.",
_ => "The ring marks the affected part; the exact spot is not known."
};
+2
View File
@@ -21,6 +21,7 @@ public partial class PostVerificationForm : Form
{
(PostVerificationKind.Incomplete, "Incomplete checks"),
(PostVerificationKind.Overlap, "Overlaps"),
(PostVerificationKind.OffPlate, "Off the plate"),
(PostVerificationKind.RapidCrossing, "Rapid crossings"),
(PostVerificationKind.MissingLeadIn, "Missing lead-ins"),
(PostVerificationKind.NestedCutOrder, "Nested part order"),
@@ -28,6 +29,7 @@ public partial class PostVerificationForm : Form
(PostVerificationKind.FixedLead, "Fixed leads"),
(PostVerificationKind.PierceZone, "Pierce zones"),
(PostVerificationKind.PartSpacing, "Part spacing"),
(PostVerificationKind.EdgeSpacing, "Edge spacing"),
(PostVerificationKind.PartSupport, "Part support"),
};
+20 -5
View File
@@ -7,7 +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, and parts closer than the plate's part spacing. Findings identify plate and part
order, parts closer than the plate's part spacing, and parts off the plate or inside
its 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
@@ -23,7 +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. Rapid warnings
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
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;
@@ -36,7 +38,7 @@ clears the highlight and restores the original plate, zoom, and overlap display
Validation does not change placements, programs, lead-ins, or cutting order, save
the nest, or write CNC output. It includes material overlaps with active clamps and
cut-off or rapid crossings of their footprints, but does not check minimum spacing
to clamps or plate edges, or whether parts lie within the plate boundaries.
to clamps.
It does not verify a post processor's final output. Posting still runs its own
fresh verification and consent flow.
@@ -118,9 +120,22 @@ shows the first three check categories, even when there are no findings:
than `SpacingSlack`, which lets interactive fills that place arcs exactly at the spacing
pass. Auto Nest's layout check and the whole-job runner's candidate validation confirm the
same way, so all three agree. Zero spacing allows contact.
A part inside another's cutout is held to the spacing from the hole edge. Cut-offs, the
plate edge and clamps are not spacing-checked. The finding gives the measured gap; this
A part inside another's cutout is held to the spacing from the hole edge. Cut-offs and
clamps are not spacing-checked; the plate edge has its own check below. The finding gives the measured gap; this
line appears in the summary only when there is such a finding.
- **Off the plate:** a part whose placed program cuts, leads or scribes past the sheet
(native curve bounds, beyond `SpacingSlack`), or, on an [outlined sheet](irregular-stock.md),
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 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.
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
host), proven on clean material exactly as Plan Cutting proves its inner-before-host
prerequisite. **Nested part order** warns when the nested part comes after its host in the