mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 19:25:43 -05:00
Ahead of the new laser schedule, three ways a post could silently cut with the wrong process, and one way its settings silently reset: - Cincinnati CL wrote no G89 when no cut library matched the nest's material, thickness and gas (or no etch library for etch features), so the controller cut with whatever library it loaded last. It now refuses, unless the new "Require libraries" setting is turned off. - Cincinnati CL declared G20/G21 from its own setting without comparing the nest's units, so a metric nest posted 25.4x too large. It now refuses a mismatch. - CI Fiber posted any material missing from its table as the default code (MSN), so stainless or aluminum ran with mild-steel technology. A named, unmapped material is now refused; the default code covers only nests with no material. - Posts saved their settings beside their DLL, inside the install folder a Velopack update replaces, so every update reset them to the shipped defaults. Saves now go to %APPDATA%\OpenNest\Posts and the shipped JSON only seeds them; constructing a post no longer writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.6 KiB
C#
125 lines
4.6 KiB
C#
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 { RequireLibraries = false, 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);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(Posts))]
|
|
public void UnplannedDoor_IsRefusedBeforeTheOutputFileIsTouched(string post)
|
|
{
|
|
var (nest, _) = DoorNest();
|
|
var directory = Directory.CreateTempSubdirectory("display-perimeter-post-");
|
|
try
|
|
{
|
|
var path = Path.Combine(directory.FullName, "door.cnc");
|
|
File.WriteAllText(path, "PREVIOUS GOOD PROGRAM");
|
|
|
|
Assert.Throws<InvalidOperationException>(() => Post(post).Post(nest, path));
|
|
|
|
Assert.Equal("PREVIOUS GOOD PROGRAM", File.ReadAllText(path));
|
|
Assert.Single(directory.GetFiles());
|
|
}
|
|
finally
|
|
{
|
|
directory.Delete(recursive: true);
|
|
}
|
|
}
|
|
|
|
[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));
|
|
}
|
|
}
|