Files
OpenNest/OpenNest.WinForms.Tests/Forms/CadConverterBreakoffTests.cs
T
ajandClaude Opus 5.5 6f555345e3 feat(cad): cut breakoff joining lines as tabbed open break cuts
Breakoffs are now picked by their joining line, default to the smaller side as scrap
(flippable), and cut that line between tabs on a new Break layer instead of slots.
Plan Cutting and Assign Lead-ins enter each cut from the scrap with the nest's internal
lead-in, or pierce on the line without one; checks treat break cuts as intentional open
cuts that are not material.

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

141 lines
5.9 KiB
C#

using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using OpenNest.Bending;
using OpenNest.Breakoffs;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Controls;
using OpenNest.Forms;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.WinForms.Tests.Forms;
/// <summary>
/// Drives the CAD Converter's breakoff tool through its real mouse handling: a 10 x 6 part with a
/// triangular breakoff (10,1) -> (12,3) -> (10,5) joined along x = 10.
/// </summary>
public class CadConverterBreakoffTests
{
[Fact]
public void PickingTheJoiningLine_AddsABreakoff_FlipSwapsTheScrap_DraggingMovesATab() => ArchUnitsTests.RunSta(() =>
{
using var form = new BreakoffTestForm();
Show(form);
form.LoadDrawings([Part()]);
var view = Field<EntityView>(form, "entityView1");
var item = Field<FileListControl>(form, "fileList").SelectedItem!;
Invoke(form, "OnAddBreakoffClicked", null, EventArgs.Empty);
Assert.True(view.IsPickingBreakoff);
Mouse(view, "OnMouseClick", view.PointWorldToControl(new Vector(10, 3)));
Assert.Empty(form.Messages);
Assert.False(view.IsPickingBreakoff);
var breakoff = Assert.Single(item.Breakoffs);
Assert.Single(breakoff.EdgeIds);
Assert.Equal(4, breakoff.TabCenters.Count);
var layout = Assert.Single(view.BreakoffLayouts);
Assert.True(layout.IsValid, layout.Error);
Assert.Equal(4, new Shape { Entities = layout.Scrap.ToList() }.Area(), 6);
Invoke(form, "OnBreakoffFlipped", null, 0);
Assert.True(item.Breakoffs[0].Flip);
Assert.Equal(60, new Shape { Entities = view.BreakoffLayouts[0].Scrap.ToList() }.Area(), 6);
Invoke(form, "OnBreakoffFlipped", null, 0);
breakoff = item.Breakoffs[0];
layout = view.BreakoffLayouts[0];
var before = breakoff.TabCenters[1];
Mouse(view, "OnMouseDown", view.PointWorldToControl(layout.PointAt(before)));
Mouse(view, "OnMouseMove", view.PointWorldToControl(new Vector(10, 1 + 1.75)));
Mouse(view, "OnMouseUp", view.PointWorldToControl(new Vector(10, 1 + 1.75)));
Assert.InRange(breakoff.TabCenters[1], 1.6, 1.9);
Assert.NotEqual(before, breakoff.TabCenters[1]);
var drawing = Assert.Single(form.GetDrawings());
Assert.Equal(breakoff.TabCenters, Assert.Single(drawing.Breakoffs).TabCenters);
var prepared = PreparedContours.Capture(drawing.Program, new CuttingParameters());
Assert.Equal(1, prepared.Count);
Assert.Equal(3, prepared.BreakCutCount);
form.Close();
});
[Fact]
public void PickingTheOutline_ExplainsAndAddsNothing() => ArchUnitsTests.RunSta(() =>
{
using var form = new BreakoffTestForm();
Show(form);
form.LoadDrawings([Part()]);
var view = Field<EntityView>(form, "entityView1");
Invoke(form, "OnAddBreakoffClicked", null, EventArgs.Empty);
Mouse(view, "OnMouseClick", view.PointWorldToControl(new Vector(5, 0)));
Assert.StartsWith("Could not make a breakoff from that line", Assert.Single(form.Messages));
Assert.Empty(Field<FileListControl>(form, "fileList").SelectedItem!.Breakoffs);
form.Close();
});
[Fact]
public void ReopenedDrawing_ShowsItsBreakoff() => ArchUnitsTests.RunSta(() =>
{
var source = Part();
var edges = source.SourceEntities!.Where(e => e is Line { StartPoint.X: 10, EndPoint.X: 10 } l
&& l.Length == 4).ToList();
var result = new CadImportResult { Entities = source.SourceEntities!, Name = "reopened" };
var withBreakoff = CadImporter.BuildDrawing(result, source.SourceEntities!, new List<Bend>(), 1, null, null,
[new Breakoff { EdgeIds = edges.Select(e => e.Id).ToList() }]);
using var form = new BreakoffTestForm();
Show(form);
form.LoadDrawings([withBreakoff]);
var layout = Assert.Single(Field<EntityView>(form, "entityView1").BreakoffLayouts);
Assert.True(layout.IsValid, layout.Error);
form.Close();
});
private static Drawing Part()
{
var entities = new List<Entity>
{
new Line(0, 0, 10, 0), new Line(10, 0, 10, 1), new Line(10, 1, 10, 5), new Line(10, 5, 10, 6),
new Line(10, 6, 0, 6), new Line(0, 6, 0, 0), new Line(10, 1, 12, 3), new Line(12, 3, 10, 5),
};
foreach (var e in entities)
e.Layer = new Layer("0") { IsVisible = true };
return CadImporter.BuildDrawing(new CadImportResult { Entities = entities, Name = "breakoff" }, entities,
new List<Bend>(), 1, null, null);
}
private static void Show(Form form)
{
form.WindowState = FormWindowState.Normal;
form.StartPosition = FormStartPosition.Manual;
form.Location = new Point(0, 0);
form.Size = new System.Drawing.Size(1200, 800);
form.Show();
}
private static T Field<T>(object owner, string name) =>
(T)owner.GetType().BaseType!.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
private static void Invoke(CadConverterForm form, string name, params object?[] args) =>
typeof(CadConverterForm).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(form, args);
private static void Mouse(Control control, string handler, Point at) =>
typeof(Control).GetMethod(handler, BindingFlags.Instance | BindingFlags.NonPublic, [typeof(MouseEventArgs)])!
.Invoke(control, [new MouseEventArgs(MouseButtons.Left, 1, at.X, at.Y, 0)]);
private sealed class BreakoffTestForm : CadConverterForm
{
public List<string> Messages { get; } = new();
internal override bool AskBreakoffSettings(BreakoffDialog dialog) => true;
internal override void ShowBreakoffMessage(string message) => Messages.Add(message);
}
}