using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Support;
namespace OpenNest.Tests.Support;
///
/// Whether a cut part stays level on the laser support grid: its center of gravity against the
/// convex hull of the grid points under its material, and where one tab steadies it best.
///
public class SupportAnalyzerTests
{
private static SupportMaterial Rectangle(double x1, double y1, double x2, double y2, params Vector[][] holes) =>
SupportMaterial.FromPolygons(
new[] { new Vector(x1, y1), new Vector(x1, y2), new Vector(x2, y2), new Vector(x2, y1) }, holes);
private static Vector[] Square(double x1, double y1, double x2, double y2) =>
new[] { new Vector(x1, y1), new Vector(x2, y1), new Vector(x2, y2), new Vector(x1, y2) };
[Fact]
public void Square_OverManyPoints_IsSupportedByItsDistanceToTheOutermostContacts()
{
var grid = new SupportGrid(1, 1, 0.5, 0.5);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 4, 4));
Assert.True(support.IsComplete);
Assert.Equal(new Vector(2, 2), support.CenterOfGravity);
Assert.Equal(16, support.Contacts.Count);
Assert.Equal(1.5, support.Margin, 12);
Assert.True(support.IsSupported);
}
[Fact]
public void Holes_HaveNoContactsAndMoveTheCenterOfGravity()
{
var grid = new SupportGrid(1, 1, 0.5, 0.5);
var centered = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 4, 4, Square(1, 1, 3, 3)));
var offCenter = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 4, 4, Square(0.2, 0.2, 1.8, 1.8)));
Assert.Equal(12, centered.Contacts.Count);
Assert.DoesNotContain(centered.Contacts, c => c.X is > 1 and < 3 && c.Y is > 1 and < 3);
Assert.Equal(new Vector(2, 2), centered.CenterOfGravity);
Assert.True(offCenter.CenterOfGravity.X > 2 && offCenter.CenterOfGravity.Y > 2);
}
[Fact]
public void RegistrationTolerance_KeepsTheWorstShiftedMargin()
{
var grid = new SupportGrid(1, 1, 0.5, 0.5, registrationTolerance: 0.25);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 4, 4));
Assert.Equal(1.25, support.Margin, 12);
}
[Fact]
public void Strip_OnOneRowOfContacts_TipsAndATabOnTheFarEdgeSteadiesIt()
{
// One row of contacts at y = 0.25 runs under a 10 x 1 strip whose center is at y = 0.5.
var grid = new SupportGrid(2, 4, 0, 0.25, stabilityMargin: 0.1);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 1));
Assert.False(support.IsSupported);
Assert.Equal(-0.25, support.Margin, 12);
Assert.True(support.TabCanSupport);
Assert.Equal(0.25, support.BestTabMargin, 9);
Assert.Equal(1, support.BestTabPoint!.Value.Y, 9);
Assert.True(support.AcceptsTabAt(new Vector(5, 1)));
Assert.False(support.AcceptsTabAt(new Vector(5, 0)));
Assert.True(support.MarginWith(new Vector(5, 0)) < 0);
}
[Fact]
public void Strip_WhoseTabsTieAlongTheFarEdge_IsTabbedAcrossItsCenterOfGravity()
{
// The row's own edge caps the margin at 0.25, so tabs from about x = 2.5 to 7.5 on the
// far edge tie; the one across the center of gravity is chosen.
var grid = new SupportGrid(2, 4, 0, 0.25, stabilityMargin: 0.1);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 1));
Assert.Equal(0.25, support.MarginWith(new Vector(3, 1)), 9);
Assert.Equal(0.25, support.MarginWith(new Vector(7, 1)), 9);
Assert.Equal(0.25, support.BestTabMargin, 9);
Assert.Equal(1, support.BestTabPoint!.Value.Y, 9);
Assert.InRange(support.BestTabPoint!.Value.X, 4.95, 5.05);
}
[Fact]
public void Strip_RestingOnlyNearOneEnd_IsTabbedAtTheFarEndNotAcrossItsCenter()
{
// Two contacts at x = 1 hold a 10 x 2 strip whose center of gravity is at (5, 1); only a
// tab at the far end reaches past it, so the tie-break does not pull it to mid-side.
var grid = new SupportGrid(20, 1, 1, 0.5, stabilityMargin: 0.1);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 2));
Assert.Equal(-4, support.Margin, 9);
Assert.True(support.TabCanSupport);
Assert.Equal(10, support.BestTabPoint!.Value.X, 9);
Assert.InRange(support.BestTabPoint!.Value.Y, 0.9, 1.1);
Assert.True(support.MarginWith(new Vector(5, 2)) < 0);
Assert.False(support.AcceptsTabAt(new Vector(5, 2)));
}
[Fact]
public void Strip_BetweenRows_FallsThroughAndOnlyTheNearestTabIsAccepted()
{
var grid = new SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1);
var support = SupportAnalyzer.Analyze(grid, 1, Rectangle(0, 0, 10, 0.8));
Assert.Empty(support.Contacts);
Assert.Equal(double.NegativeInfinity, support.Margin);
Assert.False(support.TabCanSupport);
// The outline is sampled, so the nearest sample may sit just beside (5, 0) or (5, 0.8).
Assert.InRange(support.BestTabMargin, -0.41, -0.4);
Assert.True(support.AcceptsTabAt(support.BestTabPoint!.Value));
Assert.False(support.AcceptsTabAt(new Vector(0, 0)));
}
// The 10 x 1 strip with a 2 x 0.2 bump on its top edge: inside corners at (4, 1) and (6, 1).
private static SupportMaterial BumpedStrip() =>
SupportMaterial.FromPolygons(new[]
{
new Vector(0, 0), new Vector(0, 1), new Vector(4, 1), new Vector(4, 1.2),
new Vector(6, 1.2), new Vector(6, 1), new Vector(10, 1), new Vector(10, 0),
});
[Fact]
public void BumpedStrip_EquallyGoodTabPoints_KeepTheTabOutOfTheInsideCorners()
{
// Along the top edge the margin is held by the contact row, so the inside corners tie
// with the edge around them and, as vertices, used to be found first.
var grid = new SupportGrid(2, 4, 0, 0.25, stabilityMargin: 0.1);
var support = SupportAnalyzer.Analyze(grid, 1, BumpedStrip(), insideCornerClearance: 0.25);
Assert.Equal(new[] { new Vector(4, 1), new Vector(6, 1) }, support.InsideCorners.OrderBy(c => c.X));
Assert.True(support.TabCanSupport);
Assert.Equal(support.MarginWith(new Vector(4, 1)), support.BestTabMargin, 9);
var best = support.BestTabPoint!.Value;
Assert.True(best.DistanceTo(new Vector(4, 1)) > 0.5 && best.DistanceTo(new Vector(6, 1)) > 0.5, best.ToString());
Assert.False(support.AcceptsTabAt(new Vector(4, 1)));
Assert.False(support.AcceptsTabAt(new Vector(6.2, 1)));
Assert.True(support.AcceptsTabAt(new Vector(6.3, 1)));
}
[Fact]
public void InsideCorners_CountAChordedFilletByItsWholeTurnAndIgnoreShallowBends()
{
// An L whose inside corner is a quarter-round fillet chorded into 6 small turns, and a
// 10 degree inward bend along its bottom edge.
var outline = new List { new(0, 0), new(5, 0.4), new(10, 0), new(10, 2) };
for (var i = 0; i <= 6; i++)
{
var angle = -System.Math.PI / 2 - i * System.Math.PI / 12;
outline.Add(new Vector(3 + System.Math.Cos(angle), 3 + System.Math.Sin(angle)));
}
outline.Add(new Vector(2, 10));
outline.Add(new Vector(0, 10));
var corners = SupportAnalyzer.InsideCorners(outline);
Assert.Equal(7, corners.Length);
Assert.All(corners, c => Assert.Equal(1, c.DistanceTo(new Vector(3, 3)), 9));
Assert.Equal(corners, SupportAnalyzer.InsideCorners(Enumerable.Reverse(outline).ToList()).OrderBy(outline.IndexOf));
}
[Theory]
[InlineData(1, 1, 1)]
[InlineData(2, -1, 1)]
[InlineData(3, -1, -1)]
[InlineData(4, 1, -1)]
public void Quadrant_MeasuresTheGridFromTheOriginIntoTheSheet(int quadrant, double signX, double signY)
{
var grid = new SupportGrid(1, 1, 0.25, 0.75);
var sheet = new Box(signX < 0 ? -4 : 0, signY < 0 ? -4 : 0, 4, 4);
var points = grid.PointsIn(sheet, quadrant);
Assert.Equal(16, points.Count);
Assert.Contains(new Vector(signX * 0.25, signY * 0.75), points);
Assert.Contains(new Vector(signX * 3.25, signY * 3.75), points);
}
[Fact]
public void ProgramMaterial_IsPlacedAtItsLocationWithoutLeads()
{
var program = new Program();
program.MoveTo(-1, 2);
program.Codes.Add(new LinearMove(0, 2) { Layer = LayerType.Leadin });
program.LineTo(0, 4); program.LineTo(4, 4); program.LineTo(4, 0); program.LineTo(0, 0); program.LineTo(0, 2);
var material = SupportMaterial.FromProgram(program, new Vector(10, 20));
Assert.True(material.IsComplete, material.Reason);
Assert.True(material.TryGetCenterOfGravity(out var center, out var area));
Assert.Equal(16, area, 9);
Assert.Equal(12, center.X, 9);
Assert.Equal(22, center.Y, 9);
}
[Fact]
public void Grid_RejectsNonPositivePitch()
{
Assert.Throws(() => new SupportGrid(0, 1));
Assert.Throws(() => new SupportGrid(1, 1, registrationTolerance: -1));
Assert.Equal(new SupportGrid(2, 2, 1, 1, 0.5, 0.25), new SupportGrid(1, 1, 0.5, 0.5, 0.25, 0.125).Scaled(2));
}
}