mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 00:15:46 -05:00
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:
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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
};
|
||||
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user