Files
OpenNest/OpenNest.WinForms.Tests/Actions/ActionTrimExtendCutOffTests.cs
T
ajandClaude Opus 5.5 4028dafb26 feat(cutoffs): trim a middle piece into a gap in the line
Trim refused the piece between two crossing cut-offs because a cut-off had
one start and one end, so a line could not be stopped short around a pocket
of parts without deleting it and drawing two. A cut-off now carries Gaps,
stretches between its limits left uncut: segment generation excludes them
like parts, so the line splits into segments that each take their own place
in the cut sequence. Trimming a middle piece adds a gap from one crossing
centre to the other (merged with any it touches), Extend near a gap's edge
closes it, and Swap Through-Cut refuses a junction a gap touches rather
than guess which side of the gap should run through.

Every reader of the limits treats a gap as uncut, so nothing waits for,
warns about or credits a cut that never happens: segment spans stop at a
gap, the cutting dependencies skip a part inside one, the clamp check
ignores a clamp inside one, the cutting-state and overlap stamps go stale
when gaps change, and a gapped line is never full span for the expected
remnant or automatic cut-offs. Gaps persist as an optional gaps array;
older versions ignore it and would cut straight through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:59:46 -04:00

115 lines
5.0 KiB
C#

using System.Reflection;
using System.Windows.Forms;
using OpenNest.Actions;
using OpenNest.Controls;
using OpenNest.Geometry;
using OpenNest.Help;
namespace OpenNest.WinForms.Tests.Actions;
/// <summary>
/// Trim/Extend Cut-Off says which mode it is in and switches with Tab; right-click on a junction
/// offers Swap Through-Cut.
/// </summary>
public class ActionTrimExtendCutOffTests
{
// Default view scale is about one pixel per inch: allow for rounding to whole pixels.
private const double Tolerance = 1.0;
private const double SeparatorY = 31.510593;
private const double LastVerticalX = 70.221406;
private static void PressKey(PlateView view, Keys key) =>
typeof(Control).GetMethod("OnKeyDown", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(view, new object[] { new KeyEventArgs(key) });
private static bool IsInputKey(PlateView view, Keys key) =>
(bool)typeof(PlateView).GetMethod("IsInputKey", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(view, new object[] { key })!;
// N26-264 plate 1: the vertical at X 70.22 stops at the full-length horizontal separator.
private static (Plate Plate, CutOff Vertical, CutOff Horizontal) Junction()
{
var plate = new Plate(48, 96);
var vertical = new CutOff(new Vector(LastVerticalX, 0), CutOffAxis.Vertical) { EndLimit = SeparatorY };
var horizontal = new CutOff(new Vector(0, SeparatorY), CutOffAxis.Horizontal);
plate.CutOffs.Add(vertical);
plate.CutOffs.Add(horizontal);
plate.RegenerateCutOffs(new CutOffSettings());
return (plate, vertical, horizontal);
}
[Fact]
public void StatusNamesTheModeAndTabSwitchesIt() => StaTestThread.Run(() =>
{
using var view = new PlateView { Plate = new Plate(48, 96) };
Assert.False(IsInputKey(view, Keys.Tab));
view.SetAction(typeof(ActionTrimExtendCutOff));
Assert.Equal(
"Trim/Extend Cut-Off: click the piece of a cut-off to remove, Tab switches to Extend, Esc finishes, F1 for help",
view.Status);
Assert.Equal(HelpTopics.TrimExtendCutOff, view.ActionHelpTopic);
Assert.True(IsInputKey(view, Keys.Tab));
PressKey(view, Keys.Tab);
Assert.StartsWith("Trim/Extend Cut-Off: click near the end of a cut-off to extend it or close a gap, Tab switches to Trim",
view.Status);
PressKey(view, Keys.Escape);
Assert.IsType<ActionSelect>(view.Actions.CurrentAction);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
[Fact]
public void ApplyRegeneratesAndRefusalsChangeNothing() => StaTestThread.Run(() =>
{
var (plate, vertical, horizontal) = Junction();
using var view = new PlateView { Plate = plate };
var refused = CutOffTrim.Trim(plate, vertical, new Vector(LastVerticalX, 10), view.CutOffSettings);
Assert.False(view.CutOffs.Apply(refused, "Trim Cut-Off"));
Assert.Equal(SeparatorY, vertical.EndLimit);
Assert.True(view.CutOffs.Apply(
CutOffTrim.Trim(plate, horizontal, new Vector(90, SeparatorY), view.CutOffSettings), "Trim Cut-Off"));
Assert.Equal(LastVerticalX, horizontal.EndLimit);
var right = horizontal.Drawing.Program.Codes.OfType<OpenNest.CNC.LinearMove>().Max(move => move.EndPoint.X);
Assert.Equal(LastVerticalX, right, 6);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
[Fact]
public void RightClickOnAJunctionSwapsTheThroughCut() => StaTestThread.Run(() =>
{
var (plate, vertical, horizontal) = Junction();
using var view = new PlateView { Plate = plate };
var menu = view.CutOffMenu.BuildMenu(view.PointWorldToControl(new Vector(LastVerticalX, SeparatorY)), Tolerance);
Assert.NotNull(menu);
var swap = Assert.Single(menu.Items.Cast<ToolStripItem>());
Assert.Equal("Swap Through-Cut", swap.Text);
swap.PerformClick();
Assert.Null(vertical.EndLimit);
Assert.Equal(LastVerticalX, horizontal.EndLimit);
// The vertical now splits the sheet, so it is cut last.
Assert.Same(vertical.Drawing, plate.Parts[^1].BaseDrawing);
Assert.Equal("Swapped the through-cut", view.Status);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
[Fact]
public void OffersNoSwapAwayFromAJunctionOrOutsideSelectMode() => StaTestThread.Run(() =>
{
var (plate, _, _) = Junction();
using var view = new PlateView { Plate = plate };
var onJunction = view.PointWorldToControl(new Vector(LastVerticalX, SeparatorY));
Assert.Null(view.CutOffMenu.BuildMenu(view.PointWorldToControl(new Vector(20, 10)), Tolerance));
view.InspectionOnly = true;
Assert.Null(view.CutOffMenu.BuildMenu(onJunction, Tolerance));
view.InspectionOnly = false;
view.Actions.SetAction(typeof(ActionZoomWindow));
Assert.Null(view.CutOffMenu.BuildMenu(onJunction, Tolerance));
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
}