From 15055aef4d946292b94bb45705f7c55e2b411795 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 13 Oct 2026 19:21:37 -0400 Subject: [PATCH] feat(cutoffs): measure a cut-off line without building its program --- OpenNest.Core/CutOff.cs | 11 ++++ .../CutOffs/CutOffMeasurementTests.cs | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 OpenNest.Tests/CutOffs/CutOffMeasurementTests.cs diff --git a/OpenNest.Core/CutOff.cs b/OpenNest.Core/CutOff.cs index b7d375a..e967fbb 100644 --- a/OpenNest.Core/CutOff.cs +++ b/OpenNest.Core/CutOff.cs @@ -39,6 +39,17 @@ namespace OpenNest Drawing.Program = program; } + /// Measures the same trimmed, filtered segments as regeneration without building a program. + internal (double Length, int SegmentCount) MeasureSegments( + Plate plate, + CutOffSettings settings, + Dictionary cache = null + ) + { + var segments = ComputeSegments(plate, settings, cache); + return (segments.Sum(s => s.End - s.Start), segments.Count); + } + private string GetName() { var axisChar = Axis == CutOffAxis.Vertical ? "V" : "H"; diff --git a/OpenNest.Tests/CutOffs/CutOffMeasurementTests.cs b/OpenNest.Tests/CutOffs/CutOffMeasurementTests.cs new file mode 100644 index 0000000..3b9db02 --- /dev/null +++ b/OpenNest.Tests/CutOffs/CutOffMeasurementTests.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Tests.CutOffs; + +public class CutOffMeasurementTests +{ + [Theory] + [InlineData(1, false, false)] + [InlineData(2, false, false)] + [InlineData(3, false, false)] + [InlineData(4, false, false)] + [InlineData(1, true, false)] + [InlineData(2, true, false)] + [InlineData(3, true, false)] + [InlineData(4, true, false)] + [InlineData(1, false, true)] + [InlineData(2, false, true)] + [InlineData(3, false, true)] + [InlineData(4, false, true)] + public void Measurement_MatchesRegeneratedSegmentsWithoutMutatingDrawing(int quadrant, bool limited, bool excluded) + { + var plate = new Plate(60, 120) { Quadrant = quadrant }; + var negativeX = quadrant is 2 or 3; + var negativeY = quadrant is 3 or 4; + var program = new Program(); + program.Codes.Add(new RapidMove(0, 0)); + program.Codes.Add(new LinearMove(20, 0)); + program.Codes.Add(new LinearMove(20, 20)); + program.Codes.Add(new LinearMove(0, 20)); + program.Codes.Add(new LinearMove(0, 0)); + plate.Parts.Add(new Part(new Drawing("measurement rectangle", program), + new Vector(negativeX ? -40 : 20, negativeY ? -40 : 20))); + var bounds = plate.BoundingBox(false); + var cut = new CutOff(new Vector(negativeX ? -30 : 30, 0), CutOffAxis.Vertical); + var settings = new CutOffSettings + { + PartClearance = excluded ? 100 : 0.5, + Overtravel = 2, + MinSegmentLength = limited ? 21 : 0, + CutDirection = quadrant % 2 == 0 ? CutDirection.TowardOrigin : CutDirection.AwayFromOrigin, + }; + if (limited) + { + cut.StartLimit = bounds.Bottom + 5; + cut.EndLimit = bounds.Top - 5; + } + var cache = Plate.BuildPerimeterCache(plate); + var original = cut.Drawing.Program; + // Reflection keeps the first run behavioral-red on the unchanged production tree. + var method = typeof(CutOff).GetMethod("MeasureSegments", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); + var measured = Assert.IsType>(method.Invoke(cut, new object[] { plate, settings, cache })); + Assert.Same(original, cut.Drawing.Program); + cut.Regenerate(plate, settings, cache); + var length = Enumerable.Range(0, cut.Drawing.Program.Codes.Count / 2).Sum(i => + System.Math.Abs(Assert.IsType(cut.Drawing.Program.Codes[2 * i + 1]).EndPoint.Y - + Assert.IsType(cut.Drawing.Program.Codes[2 * i]).EndPoint.Y)); + Assert.Equal(length, measured.Item1, 10); + Assert.Equal(cut.Drawing.Program.Codes.Count / 2, measured.Item2); + if (excluded) + Assert.Equal((0.0, 0), measured); + } +}