Files
OpenNest/OpenNest.WinForms.Tests/GraphicsHelperPlacementTests.cs
T
aj f4d5cfe98e fix(ui): translate absolute-coordinate part paths by placement
Build paths in the drawing-local frame and translate both ordinary and split cut/lead paths once, without changing CNC programs. Add native Windows regressions using the operator's synthetic G90 fixture and incremental controls. Four absolute-mode cases fail before the fix; all seven pass after.
2026-10-07 17:22:06 -04:00

156 lines
6.3 KiB
C#

using System.Drawing;
using System.Drawing.Drawing2D;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.WinForms.Tests;
public class GraphicsHelperPlacementTests
{
[Theory]
[InlineData(Mode.Absolute, false)]
[InlineData(Mode.Absolute, true)]
[InlineData(Mode.Incremental, false)]
[InlineData(Mode.Incremental, true)]
public void PlacementTranslatesEveryOutlineAndLeadPointWithoutChangingProgram(Mode mode, bool splitPaths)
{
var program = new CNC.Program(Mode.Absolute);
program.Codes.AddRange(new ICode[]
{
new RapidMove(-1, 0),
new LinearMove(0, 0) { Layer = LayerType.Leadin },
new LinearMove(0, 4),
new LinearMove(3, 4),
new ArcMove(4, 3, 3, 3, RotationType.CW),
new LinearMove(4, 0),
new LinearMove(0, 0),
new ArcMove(-1, -1, -1, 0, RotationType.CW) { Layer = LayerType.Leadout },
new RapidMove(3, 2),
new ArcMove(3, 2, 2, 2),
});
program.Mode = mode;
var before = NestWriter.GetProgramText(program);
var instructions = program.Codes.ToArray();
using var local = program.GetGraphicsPath();
program.GetGraphicsPaths(Vector.Zero, out var localCut, out var localLead);
using (localCut)
using (localLead)
{
Assert.True(local.PointCount > 0);
Assert.True(localCut.PointCount > 0);
Assert.True(localLead.PointCount > 0);
foreach (var origin in new[] { new Vector(10.25, 6.5), new Vector(-7.5, 12.25), Vector.Zero })
{
if (!splitPaths)
{
using var placed = program.GetGraphicsPath(origin);
AssertTranslated(local, placed, origin);
}
else
{
program.GetGraphicsPaths(origin, out var cut, out var lead);
using (cut)
using (lead)
{
AssertTranslated(localCut, cut, origin);
AssertTranslated(localLead, lead, origin);
}
}
}
}
Assert.Equal(before, NestWriter.GetProgramText(program));
Assert.Equal(instructions, program.Codes);
Assert.Equal(mode, program.Mode);
}
[Fact]
public void IncrementalSubprogramOffsetsStayRelativeToProgramOrigin()
{
var hole = new CNC.Program(Mode.Incremental);
hole.Codes.AddRange(new ICode[]
{
new RapidMove(1, 0),
new ArcMove(0, 0, -1, 0),
});
var program = new CNC.Program(Mode.Incremental);
program.Codes.Add(new SubProgramCall(hole, 0) { Offset = new Vector(3, 4) });
program.Codes.Add(new SubProgramCall(hole, 0) { Offset = new Vector(9, 4) });
var before = NestWriter.GetSubProgramsText(program);
using var local = program.GetGraphicsPath();
using var placed = program.GetGraphicsPath(new Vector(20, 30));
AssertTranslated(local, placed, new Vector(20, 30));
Assert.Equal(new RectangleF(22, 33, 8, 2), placed.GetBounds());
program.GetGraphicsPaths(new Vector(20, 30), out var cut, out var lead);
using (cut)
using (lead)
{
AssertTranslated(local, cut, new Vector(20, 30));
Assert.Equal(0, lead.PointCount);
}
Assert.Same(hole, ((SubProgramCall)program.Codes[0]).Program);
Assert.Same(hole, ((SubProgramCall)program.Codes[1]).Program);
Assert.Equal(before, NestWriter.GetSubProgramsText(program));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void SavedAbsolutePartsFollowRepeatedMovesAndViewTransform(bool splitPaths) => StaTestThread.Run(() =>
{
var fixture = Path.Combine(AppContext.BaseDirectory, "Fixtures", "absolute-coordinate-parts.nest");
var nest = new NestReader(fixture).Read();
Assert.Equal(2, nest.Plates.Count);
Assert.Equal(3, nest.Plates.Sum(plate => plate.Parts.Count));
using var view = new PlateView();
view.Matrix.Reset();
view.Matrix.Scale(2, -2);
view.Matrix.Translate(40, 80, MatrixOrder.Append);
foreach (var part in nest.Plates.SelectMany(plate => plate.Parts))
{
Assert.Equal(Mode.Absolute, part.Program.Mode);
part.HasManualLeadIns = splitPaths;
var before = NestWriter.GetProgramText(part.Program);
var initial = part.Location;
var layout = LayoutPart.Create(part, view);
try
{
foreach (var location in new[] { initial, new Vector(4.25, 10.75), new Vector(15.5, 3.25), initial })
{
part.Location = location;
layout.Path.Dispose();
layout.Update(view);
using var expected = part.Program.GetGraphicsPath();
using var translation = new Matrix();
translation.Translate((float)location.X, (float)location.Y);
expected.Transform(translation);
expected.Transform(view.Matrix);
AssertTranslated(expected, layout.Path, Vector.Zero);
if (splitPaths)
Assert.Equal(0, layout.LeadInPath.PointCount);
Assert.Equal(before, NestWriter.GetProgramText(part.Program));
}
}
finally
{
layout.Path.Dispose();
layout.LeadInPath?.Dispose();
}
}
}, TimeSpan.FromMinutes(1), "Absolute-coordinate placement test did not complete.");
private static void AssertTranslated(GraphicsPath local, GraphicsPath placed, Vector offset)
{
Assert.Equal(local.PointCount, placed.PointCount);
Assert.Equal(local.PathTypes, placed.PathTypes);
var expected = local.PathPoints;
var actual = placed.PathPoints;
for (var i = 0; i < expected.Length; i++)
{
Assert.InRange(actual[i].X - (expected[i].X + offset.X), -0.00001, 0.00001);
Assert.InRange(actual[i].Y - (expected[i].Y + offset.Y), -0.00001, 0.00001);
}
}
}