mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 10:11:35 -05:00
fix(posts): never post a display perimeter
The posts treat every non-lead feed move as a cut, so an applied blank's display tail, or the outline of a blank posted without a plan, would have been cut along the blank's edge. The pre-post review counted that outline as an ordinary cutting contour. Cincinnati, CI Fiber and the engraver now read part programs through DisplayPerimeterTail.ForPosting: an applied program posts exactly what the same program posts with its tail removed by hand, and an unplanned display perimeter is refused with a message that names the part and points to Plan Cutting. The pre-post review reports the unplanned outline by name.
This commit is contained in:
1 parent
684207039a
commit
6201065034
6 files changed
+122
-4
No files matched your search
@@ -135,6 +135,11 @@ public static class PostVerificationAnalyzer
|
||||
if (part == null || !double.IsFinite(part.Rotation))
|
||||
throw new ArgumentException("Missing part or invalid rotation.");
|
||||
var moves = ExecutionMotionReader.Read(part.Program, part.Location, position, cancellationToken).Motions;
|
||||
// Executed reads stop at an applied program's display tail, so any display
|
||||
// perimeter left is an unplanned blank whose outline a post would refuse.
|
||||
if (moves.Any(move => move.Layer == LayerType.DisplayPerimeter))
|
||||
Incomplete("This part has a display perimeter (a supplied blank's outline that is never cut) "
|
||||
+ "but no cutting plan; posts refuse it. Run Plan Cutting so only its cutouts are cut.");
|
||||
if (expectsCuts && !moves.Any(move => !move.Rapid
|
||||
&& move.Layer is LayerType.Cut or LayerType.Display
|
||||
&& move.Curve.Length > PostVerificationGeometry.Epsilon))
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
using OpenNest.Posts.CincinnatiCIFiber;
|
||||
using OpenNest.Posts.GravographIS;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
/// <summary>No post may cut a supplied blank's display perimeter, planned or not.</summary>
|
||||
public class DisplayPerimeterPostTests
|
||||
{
|
||||
public static IEnumerable<object[]> Posts() => [["cincinnati"], ["cifiber"], ["gravograph"]];
|
||||
|
||||
private static IPostProcessor Post(string name) => name switch
|
||||
{
|
||||
"cincinnati" => new CincinnatiPostProcessor(new CincinnatiPostConfig { ConfigurationName = "CL940", PostedAccuracy = 4 }),
|
||||
"cifiber" => new CIFiberPostProcessor(new CIFiberPostConfig { ConfigurationName = "CI FIBER 8K" }),
|
||||
_ => new GravographISPostProcessor(),
|
||||
};
|
||||
|
||||
private static string Output(string post, Nest nest)
|
||||
{
|
||||
// The engraver's writer refuses this fixture's sheet for its work envelope, so compare the
|
||||
// geometry its extractor hands the writer: the step that reads part programs.
|
||||
if (post == "gravograph")
|
||||
return string.Join("\n", new NestPolylineExtractor().ExtractLayered(nest).Select(poly =>
|
||||
poly.Layer + ":" + string.Join(";", poly.Points.Select(v => FormattableString.Invariant($"{v.X:R},{v.Y:R}")))));
|
||||
using var stream = new MemoryStream();
|
||||
Post(post).Post(nest, stream);
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
|
||||
private static (Nest Nest, Part Part) DoorNest()
|
||||
{
|
||||
var nest = new Nest("Door") { Units = Units.Inches };
|
||||
var drawing = new Drawing("door", DisplayPerimeterTests.Door());
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(new Size(20, 30));
|
||||
var part = new Part(drawing, Vector.Zero);
|
||||
plate.Parts.Add(part);
|
||||
nest.Plates.Add(plate);
|
||||
return (nest, part);
|
||||
}
|
||||
|
||||
private static (Nest Nest, Part Part) PlannedDoorNest()
|
||||
{
|
||||
var (nest, part) = DoorNest();
|
||||
var proposal = CuttingPlanBatch.Capture([nest.Plates[0]], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
||||
Assert.True(DisplayPerimeterTail.HasTail(part.Program));
|
||||
return (nest, part);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Posts))]
|
||||
public void PlannedDoor_PostsExactlyItsCutsAndNeverTheOutline(string post)
|
||||
{
|
||||
var (planned, _) = PlannedDoorNest();
|
||||
var (stripped, part) = PlannedDoorNest();
|
||||
// The same applied plan with its tail removed by hand: what the machine may run.
|
||||
Assert.True(part.RestoreLeadInProgram(DisplayPerimeterTail.WithoutTail(part.Program), locked: true));
|
||||
|
||||
Assert.Equal(Output(post, stripped), Output(post, planned));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Posts))]
|
||||
public void UnplannedDoor_IsRefusedByEveryPost(string post)
|
||||
{
|
||||
var (nest, _) = DoorNest();
|
||||
|
||||
var error = Assert.Throws<InvalidOperationException>(() => Output(post, nest));
|
||||
|
||||
Assert.Contains("display perimeter", error.Message);
|
||||
Assert.Contains("Plan Cutting", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnplannedDoor_PrePostReviewNamesTheDisplayPerimeter()
|
||||
{
|
||||
var (nest, _) = DoorNest();
|
||||
|
||||
var report = PostVerificationAnalyzer.Analyze(nest);
|
||||
|
||||
Assert.Contains(report.Findings, f => f.Message.Contains("display perimeter", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.False(report.CanPost(false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlannedDoor_PrePostReviewIsClean()
|
||||
{
|
||||
var (nest, _) = PlannedDoorNest();
|
||||
|
||||
var report = PostVerificationAnalyzer.Analyze(nest);
|
||||
|
||||
Assert.DoesNotContain(report.Findings, f => f.Message.Contains("display perimeter", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
var pgm = PostedCopy(part.Program);
|
||||
var pgm = PostedCopy(FeatureUtils.PostedProgram(part));
|
||||
var bbox = pgm.BoundingBox();
|
||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
|
||||
@@ -222,7 +222,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
if (!part.BaseDrawing.IsCutOff)
|
||||
Register(part.Program);
|
||||
Register(FeatureUtils.PostedProgram(part));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,16 @@ public static class FeatureUtils
|
||||
/// Splits a part's program into features by rapids, classifies each as etch or cut,
|
||||
/// and orders etch features before cut features. The part's program is not modified.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// The part program this post may write: a display perimeter's never-cut tail removed, and an
|
||||
/// unplanned display perimeter refused (<see cref="DisplayPerimeterTail.ForPosting"/>).
|
||||
/// </summary>
|
||||
public static Program PostedProgram(Part part) =>
|
||||
DisplayPerimeterTail.ForPosting(part.Program, $"Part '{part.BaseDrawing?.Name}'");
|
||||
|
||||
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part)
|
||||
{
|
||||
var codes = RemoveSuppressed(AbsoluteCodes(part.Program));
|
||||
var codes = RemoveSuppressed(AbsoluteCodes(PostedProgram(part)));
|
||||
InsertLeadingRapid(codes);
|
||||
|
||||
return ClassifyAndOrder(SplitByRapids(codes));
|
||||
|
||||
@@ -52,7 +52,9 @@ namespace OpenNest.Posts.CincinnatiCIFiber
|
||||
throw new ArgumentNullException(nameof(part));
|
||||
|
||||
var contours = new List<CIFiberContour>();
|
||||
Flatten(part.Program, part.Location, Vector.Zero, contours);
|
||||
// A display perimeter's tail is never cut; an unplanned one is refused.
|
||||
var program = DisplayPerimeterTail.ForPosting(part.Program, $"Part '{part.BaseDrawing?.Name}'");
|
||||
Flatten(program, part.Location, Vector.Zero, contours);
|
||||
return contours;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace OpenNest.Posts.GravographIS
|
||||
var program = part.Program;
|
||||
if (program == null)
|
||||
return;
|
||||
// A display perimeter's tail is never cut; an unplanned one is refused.
|
||||
program = DisplayPerimeterTail.ForPosting(program, $"Part '{part.BaseDrawing?.Name}'");
|
||||
|
||||
// The walk below treats Motion.EndPoint as absolute. Convert a working
|
||||
// copy to absolute mode so G91 programs (the form OpenNest's UI writes)
|
||||
|
||||
Reference in new issue
Block a user