diff --git a/OpenNest.Engine/BestFit/BestFitResult.cs b/OpenNest.Engine/BestFit/BestFitResult.cs index 60fc093..f819edb 100644 --- a/OpenNest.Engine/BestFit/BestFitResult.cs +++ b/OpenNest.Engine/BestFit/BestFitResult.cs @@ -1,6 +1,9 @@ +using OpenNest.Engine; +using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; +using System.Linq; namespace OpenNest.Engine.BestFit { @@ -54,6 +57,68 @@ namespace OpenNest.Engine.BestFit return new List { part1, part2 }; } + + public List BuildCanonicalParts() + { + return NormalizeToCutOrigin(BuildParts(Candidate.Drawing)); + } + + public List BuildSourceParts(Drawing drawing) + { + var parts = BuildCanonicalParts(); + var sourceAngle = drawing?.Source?.Angle ?? 0.0; + + for (var i = 0; i < parts.Count; i++) + { + var p = parts[i]; + var rebound = Part.CreateAtOrigin(drawing, p.Rotation); + var delta = p.BoundingBox.Location - rebound.BoundingBox.Location; + rebound.Offset(delta); + rebound.UpdateBounds(); + parts[i] = rebound; + } + + return NormalizeToCutOrigin(CanonicalFrame.FromCanonical(parts, sourceAngle)); + } + + public Box GetCutBounds(List parts) + { + return GetCutBoundingBox(parts); + } + + private static List NormalizeToCutOrigin(List parts) + { + if (parts == null || parts.Count == 0) + return parts; + + var bounds = GetCutBoundingBox(parts); + var offset = new Vector(-bounds.Left, -bounds.Bottom); + + foreach (var part in parts) + part.Offset(offset); + + return parts; + } + + private static Box GetCutBoundingBox(List parts) + { + var entities = new List(); + + foreach (var part in parts) + { + var partEntities = ConvertProgram.ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); + + foreach (var entity in partEntities) + { + entity.Offset(part.Location); + entities.Add(entity); + } + } + + return entities.GetBoundingBox(); + } } public enum BestFitSortField diff --git a/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs new file mode 100644 index 0000000..3162ac3 --- /dev/null +++ b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs @@ -0,0 +1,72 @@ +using OpenNest.Engine; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; +using OpenNest.Math; +using OpenNest.Shapes; + +namespace OpenNest.Tests.BestFit; + +public class BestFitResultFrameTests +{ + [Fact] + public void BuildCanonicalParts_NonAxisAlignedPairNormalizesActualBounds() + { + var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing(); + var canonical = CanonicalFrame.AsCanonicalCopy(drawing); + + var result = EvaluateOffsetPair(canonical, new Vector(40, 30)); + + Assert.True(IsNonAxisAligned(result.OptimalRotation), + $"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees."); + + var parts = result.BuildCanonicalParts(); + var bounds = result.GetCutBounds(parts); + + Assert.Equal(0, bounds.Left, 3); + Assert.Equal(0, bounds.Bottom, 3); + Assert.Equal(result.BoundingWidth, bounds.Length, 2); + Assert.Equal(result.BoundingHeight, bounds.Width, 2); + } + + [Fact] + public void BuildSourceParts_RebindsCanonicalResultToRotatedSourceDrawing() + { + var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing(); + drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center); + drawing.RecomputeCanonicalAngle(); + + var canonical = CanonicalFrame.AsCanonicalCopy(drawing); + var result = EvaluateOffsetPair(canonical, new Vector(40, 30)); + + var parts = result.BuildSourceParts(drawing); + var bounds = result.GetCutBounds(parts); + + Assert.All(parts, p => Assert.Same(drawing, p.BaseDrawing)); + Assert.Equal(0, bounds.Left, 3); + Assert.Equal(0, bounds.Bottom, 3); + Assert.False(parts[0].Intersects(parts[1], out _)); + } + + private static BestFitResult EvaluateOffsetPair(Drawing drawing, Vector offset) + { + var candidate = new PairCandidate + { + Drawing = drawing, + Part1Rotation = 0, + Part2Rotation = System.Math.PI, + Part2Offset = offset, + Spacing = 0.25 + }; + + return new PairEvaluator().Evaluate(candidate); + } + + private static bool IsNonAxisAligned(double angle) + { + var normalized = Angle.NormalizeRad(angle); + var nearestQuadrant = Angle.HalfPI * System.Math.Round(normalized / Angle.HalfPI); + var delta = System.Math.Abs(normalized - nearestQuadrant); + delta = System.Math.Min(delta, Angle.HalfPI - delta); + return delta > Angle.ToRadians(1); + } +} diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index 3599b98..1851f23 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -45,6 +45,7 @@ namespace OpenNest.Forms public BestFitResult SelectedResult { get; private set; } public Drawing SelectedDrawing => activeDrawing; + public List SelectedParts { get; private set; } public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches) { @@ -318,12 +319,12 @@ namespace OpenNest.Forms var cell = new BestFitCell(colorScheme); cell.PartColor = partColor; cell.Dock = DockStyle.Fill; + + var parts = result.BuildCanonicalParts(); cell.Plate.Size = new Geometry.Size( result.BoundingHeight, result.BoundingWidth); - var parts = result.BuildParts(drawing); - foreach (var part in parts) cell.Plate.Parts.Add(part); @@ -332,6 +333,7 @@ namespace OpenNest.Forms cell.DoubleClick += (sender, e) => { SelectedResult = result; + SelectedParts = result.BuildSourceParts(drawing); DialogResult = DialogResult.OK; Close(); }; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 09f8e51..99f2e0d 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -686,7 +686,8 @@ namespace OpenNest.Forms { if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) { - var parts = form.SelectedResult.BuildParts(form.SelectedDrawing); + var parts = form.SelectedParts + ?? form.SelectedResult.BuildSourceParts(form.SelectedDrawing); activeForm.PlateView.SetAction(typeof(ActionClone), parts); } }