diff --git a/OpenNest.WinForms.Tests/Fixtures/absolute-coordinate-parts.nest b/OpenNest.WinForms.Tests/Fixtures/absolute-coordinate-parts.nest new file mode 100644 index 00000000..8de708cd Binary files /dev/null and b/OpenNest.WinForms.Tests/Fixtures/absolute-coordinate-parts.nest differ diff --git a/OpenNest.WinForms.Tests/GraphicsHelperPlacementTests.cs b/OpenNest.WinForms.Tests/GraphicsHelperPlacementTests.cs new file mode 100644 index 00000000..c5574a00 --- /dev/null +++ b/OpenNest.WinForms.Tests/GraphicsHelperPlacementTests.cs @@ -0,0 +1,155 @@ +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); + } + } +} diff --git a/OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj b/OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj index 4ea51cfa..33472225 100644 --- a/OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj +++ b/OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj @@ -15,5 +15,6 @@ + diff --git a/OpenNest/GraphicsHelper.cs b/OpenNest/GraphicsHelper.cs index 5afc00b1..ed3cf421 100644 --- a/OpenNest/GraphicsHelper.cs +++ b/OpenNest/GraphicsHelper.cs @@ -20,10 +20,12 @@ namespace OpenNest public static GraphicsPath GetGraphicsPath(this Program pgm, Vector origin) { - var path = new GraphicsPath(); - var curpos = origin; - - AddProgram(path, pgm, pgm.Mode, ref curpos); + // Program coordinates are drawing-local in either mode. Translate the + // finished path so absolute moves cannot discard the part placement. + var path = pgm.GetGraphicsPath(); + using var translation = new Matrix(); + translation.Translate((float)origin.X, (float)origin.Y); + path.Transform(translation); return path; } @@ -100,9 +102,14 @@ namespace OpenNest { cutPath = new GraphicsPath(); leadPath = new GraphicsPath(); - var curpos = origin; + var curpos = Vector.Zero; AddProgramSplit(cutPath, leadPath, pgm, pgm.Mode, ref curpos); + + using var translation = new Matrix(); + translation.Translate((float)origin.X, (float)origin.Y); + cutPath.Transform(translation); + leadPath.Transform(translation); } private static void AddProgramSplit( diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index bfa2a5cf..319df19a 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -303,7 +303,10 @@ plans every plate that has parts. Both open one dialog built on the thread it was created on rather than to whichever context is current. - `PlateView` follows `Plate.PartsReordered`: it redraws parts in the plate's order (the numbers it draws are the cutting order), rebuilds their graphics and marks the overlap - check out of date. + check out of date. Both the editor and preview build outlines and lead paths in the + drawing-local frame, then apply the part placement once. Absolute (G90) programs + therefore follow moves and clones just like incremental (G91) programs; displaying a + part does not rewrite its program or coordinate mode. ## Remaining integration boundaries