Fix best-fit viewer bounds for angled pairs
This commit is contained in:
@@ -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<Part> { part1, part2 };
|
||||
}
|
||||
|
||||
public List<Part> BuildCanonicalParts()
|
||||
{
|
||||
return NormalizeToCutOrigin(BuildParts(Candidate.Drawing));
|
||||
}
|
||||
|
||||
public List<Part> 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<Part> parts)
|
||||
{
|
||||
return GetCutBoundingBox(parts);
|
||||
}
|
||||
|
||||
private static List<Part> NormalizeToCutOrigin(List<Part> 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<Part> parts)
|
||||
{
|
||||
var entities = new List<IBoundable>();
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ namespace OpenNest.Forms
|
||||
|
||||
public BestFitResult SelectedResult { get; private set; }
|
||||
public Drawing SelectedDrawing => activeDrawing;
|
||||
public List<Part> 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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user