mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-02 08:58:48 -04:00
356 lines
14 KiB
C#
356 lines
14 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.Geometry;
|
|
|
|
public class PartGeometryDirectionalPreparationTests
|
|
{
|
|
[Theory]
|
|
[InlineData(PushDirection.Left, false, -1, 0)]
|
|
[InlineData(PushDirection.Right, false, 1, 0)]
|
|
[InlineData(PushDirection.Up, false, 0, 1)]
|
|
[InlineData(PushDirection.Down, false, 0, -1)]
|
|
[InlineData(PushDirection.Left, true, -1, 0)]
|
|
[InlineData(PushDirection.Right, true, 1, 0)]
|
|
[InlineData(PushDirection.Up, true, 0, 1)]
|
|
[InlineData(PushDirection.Down, true, 0, -1)]
|
|
public void Cardinals_PreserveOrderedEndpointsAndWinding(
|
|
PushDirection direction, bool reverse, double x, double y)
|
|
{
|
|
var part = MakePart(RectangleProgram(reverse));
|
|
var expected = FrozenGetPartLines(part, direction);
|
|
Assert.Single(expected);
|
|
AssertMatches(part, direction, new Vector(x, y), 0.001);
|
|
AssertLines(expected, FrozenGetPartLines(part, new Vector(x, y)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.1)]
|
|
[InlineData(0.001)]
|
|
[InlineData(0.00001)]
|
|
public void CurvedPerimeterAndMultipleHoles_PreserveTessellationAndShapeOrder(double tolerance)
|
|
{
|
|
var program = CurvedProgram();
|
|
program.Mode = Mode.Incremental;
|
|
var part = MakePart(program);
|
|
var expected = FrozenGetPartLines(part, PushDirection.Right, tolerance);
|
|
Assert.True(expected.Count > 3);
|
|
AssertMatches(part, PushDirection.Right, new Vector(2.75, -0.625), tolerance);
|
|
// The non-directional overload is deliberately outside this extraction.
|
|
AssertLines(FrozenGetPartLines(part, (PushDirection)123, tolerance),
|
|
PartGeometry.GetPartLines(part, tolerance));
|
|
Assert.True(FrozenGetPartLines(part, PushDirection.Right, 0.00001).Count
|
|
> FrozenGetPartLines(part, PushDirection.Right, 0.1).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaterialIncludesDisplayAndLeads_ButExcludesRapidAndScribe()
|
|
{
|
|
var material = RectangleProgram(false);
|
|
AddRectangle(material, 15, 2, 3, 2, LayerType.Display);
|
|
AddRectangle(material, 21, 3, 2, 4, LayerType.Leadin);
|
|
AddRectangle(material, 26, 1, 4, 3, LayerType.Leadout);
|
|
var clean = MakePart(material);
|
|
AddRectangle(material, 100, 120, 7, 9, LayerType.Scribe);
|
|
material.MoveTo(-200, -300);
|
|
material.MoveTo(400, 500);
|
|
var marked = MakePart(material);
|
|
foreach (var direction in new[] { PushDirection.Left, PushDirection.Right, PushDirection.Up, PushDirection.Down })
|
|
{
|
|
var expected = FrozenGetPartLines(clean, direction);
|
|
Assert.Equal(4, expected.Count);
|
|
AssertLines(expected, PartGeometry.GetPartLines(marked, direction));
|
|
AssertMatches(marked, direction, new Vector(3, -2), 0.001);
|
|
}
|
|
AssertLines(PartGeometry.GetPartLines(clean), PartGeometry.GetPartLines(marked));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(123)]
|
|
public void InvalidEnum_KeepsAllEdgesRatherThanUsingZeroVector(int value)
|
|
{
|
|
var part = MakePart(RectangleProgram(false));
|
|
var direction = (PushDirection)value;
|
|
Assert.Equal(4, FrozenGetPartLines(part, direction).Count);
|
|
Assert.Empty(FrozenGetPartLines(part, new Vector(0, 0)));
|
|
AssertMatches(part, direction, new Vector(0, 0), 0.001);
|
|
AssertLines(PartGeometry.GetPartLines(part), PartGeometry.GetPartLines(part, direction));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 0)]
|
|
[InlineData(2.75, -0.625)]
|
|
[InlineData(-19, 7)]
|
|
[InlineData(double.Epsilon, -double.Epsilon)]
|
|
[InlineData(double.MaxValue, double.MaxValue)]
|
|
[InlineData(double.PositiveInfinity, 0)]
|
|
[InlineData(0, double.NegativeInfinity)]
|
|
[InlineData(double.NaN, 1)]
|
|
public void Vectors_PreserveUnnormalizedZeroAndNonfiniteArithmetic(double x, double y)
|
|
{
|
|
var part = MakePart(RectangleProgram(true));
|
|
var direction = new Vector(x, y);
|
|
AssertMatches(part, PushDirection.Down, direction, 0.001);
|
|
if (x == 0 && y == 0 || double.IsNaN(x))
|
|
Assert.Empty(PartGeometry.GetPartLines(part, direction));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(double.PositiveInfinity, 0)]
|
|
[InlineData(0, double.NegativeInfinity)]
|
|
[InlineData(double.NaN, 1)]
|
|
public void NonfiniteTranslation_PreservesDistinctCardinalAndVectorFilters(double x, double y)
|
|
{
|
|
var part = MakePart(RectangleProgram(false));
|
|
part.Location = new Vector(x, y);
|
|
AssertMatches(part, PushDirection.Right, new Vector(1, 0), 0.001);
|
|
AssertMatches(part, (PushDirection)123, new Vector(0, 0), 0.001);
|
|
if (double.IsPositiveInfinity(x))
|
|
{
|
|
Assert.Single(PartGeometry.GetPartLines(part, PushDirection.Right));
|
|
// Infinite X makes edx NaN; multiplying that by a zero vector component
|
|
// is not equivalent to the enum filter, which reads only dy here.
|
|
Assert.Empty(PartGeometry.GetPartLines(part, new Vector(1, 0)));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public void EmptyAndDegenerateContours_PreserveFallback(int points)
|
|
{
|
|
var program = new Program();
|
|
if (points > 0)
|
|
{
|
|
program.MoveTo(1, 2);
|
|
program.LineTo(points == 1 ? 1 : 5, points == 1 ? 2 : 3);
|
|
}
|
|
var part = MakePart(program);
|
|
AssertMatches(part, (PushDirection)123, new Vector(0, 0), 0.001);
|
|
AssertMatches(part, PushDirection.Up, new Vector(double.NaN, 0), 0.001);
|
|
AssertLines(FrozenGetPartLines(part, (PushDirection)123), PartGeometry.GetPartLines(part));
|
|
}
|
|
|
|
[Fact]
|
|
public void NullPartAndMalformedProgram_PreserveExceptions()
|
|
{
|
|
Assert.Throws<NullReferenceException>(() => FrozenGetPartLines(null!, PushDirection.Right));
|
|
Assert.Throws<NullReferenceException>(() => PartGeometry.GetPartLines(null!, PushDirection.Right));
|
|
Assert.Throws<NullReferenceException>(() => FrozenGetPartLines(null!, new Vector(1, 0)));
|
|
Assert.Throws<NullReferenceException>(() => PartGeometry.GetPartLines(null!, new Vector(1, 0)));
|
|
var part = MakePart(RectangleProgram(false));
|
|
part.Program.Codes.Add(null!);
|
|
Assert.Throws<NullReferenceException>(() => FrozenGetPartLines(part, PushDirection.Right));
|
|
Assert.Throws<NullReferenceException>(() => PartGeometry.GetPartLines(part, PushDirection.Right));
|
|
Assert.Throws<NullReferenceException>(() => FrozenGetPartLines(part, new Vector(1, 0)));
|
|
Assert.Throws<NullReferenceException>(() => PartGeometry.GetPartLines(part, new Vector(1, 0)));
|
|
}
|
|
|
|
private static void AssertMatches(Part part, PushDirection cardinal, Vector vector, double tolerance)
|
|
{
|
|
var program = part.Program;
|
|
var codes = program.Codes;
|
|
var identities = codes.ToArray();
|
|
var values = ProgramBits(program);
|
|
var drawingValues = ProgramBits(part.BaseDrawing.Program);
|
|
var location = new[] { Bits(part.Location.X), Bits(part.Location.Y) };
|
|
AssertLines(FrozenGetPartLines(part, cardinal, tolerance),
|
|
PartGeometry.GetPartLines(part, cardinal, tolerance));
|
|
Assert.Equal(values, ProgramBits(program));
|
|
AssertLines(FrozenGetPartLines(part, vector, tolerance),
|
|
PartGeometry.GetPartLines(part, vector, tolerance));
|
|
Assert.Equal(values, ProgramBits(program));
|
|
Assert.Equal(drawingValues, ProgramBits(part.BaseDrawing.Program));
|
|
Assert.Equal(location, new[] { Bits(part.Location.X), Bits(part.Location.Y) });
|
|
Assert.Same(program, part.Program);
|
|
Assert.Same(codes, program.Codes);
|
|
Assert.Equal(identities.Length, codes.Count);
|
|
for (var i = 0; i < identities.Length; i++)
|
|
Assert.Same(identities[i], codes[i]);
|
|
}
|
|
|
|
private static long[] ProgramBits(Program program)
|
|
{
|
|
var values = new List<long> { (long)program.Mode, Bits(program.Rotation) };
|
|
foreach (var code in program.Codes)
|
|
{
|
|
values.Add((long)code.Type);
|
|
var motion = Assert.IsAssignableFrom<Motion>(code);
|
|
values.Add(Bits(motion.EndPoint.X));
|
|
values.Add(Bits(motion.EndPoint.Y));
|
|
values.Add(motion.Suppressed ? 1 : 0);
|
|
if (code is LinearMove line)
|
|
values.Add((long)line.Layer);
|
|
if (code is ArcMove arc)
|
|
{
|
|
values.Add((long)arc.Layer);
|
|
values.Add((long)arc.Rotation);
|
|
values.Add(Bits(arc.CenterPoint.X));
|
|
values.Add(Bits(arc.CenterPoint.Y));
|
|
}
|
|
}
|
|
return values.ToArray();
|
|
}
|
|
|
|
private static void AssertLines(IReadOnlyList<Line> expected, IReadOnlyList<Line> actual)
|
|
{
|
|
Assert.Equal(expected.Count, actual.Count);
|
|
for (var i = 0; i < expected.Count; i++)
|
|
{
|
|
// Vector.Equals is tolerance-based and cannot establish scalar-bit preservation.
|
|
Assert.Equal(Bits(expected[i].StartPoint.X), Bits(actual[i].StartPoint.X));
|
|
Assert.Equal(Bits(expected[i].StartPoint.Y), Bits(actual[i].StartPoint.Y));
|
|
Assert.Equal(Bits(expected[i].EndPoint.X), Bits(actual[i].EndPoint.X));
|
|
Assert.Equal(Bits(expected[i].EndPoint.Y), Bits(actual[i].EndPoint.Y));
|
|
}
|
|
}
|
|
|
|
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
|
|
|
|
private static Part MakePart(Program program) =>
|
|
new(new Drawing("directional preparation", program), new Vector(13.125, -7.375));
|
|
|
|
private static Program RectangleProgram(bool reverse)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(1, 2);
|
|
if (reverse)
|
|
{
|
|
program.LineTo(1, 5);
|
|
program.LineTo(5, 5);
|
|
program.LineTo(5, 2);
|
|
}
|
|
else
|
|
{
|
|
program.LineTo(5, 2);
|
|
program.LineTo(5, 5);
|
|
program.LineTo(1, 5);
|
|
}
|
|
program.LineTo(1, 2);
|
|
return program;
|
|
}
|
|
|
|
private static void AddRectangle(Program program, double x, double y, double width, double height, LayerType layer)
|
|
{
|
|
program.MoveTo(x, y);
|
|
program.Codes.Add(new LinearMove(x + width, y) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(x + width, y + height) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(x, y + height) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(x, y) { Layer = layer });
|
|
}
|
|
|
|
private static Program CurvedProgram()
|
|
{
|
|
var program = new Program();
|
|
// A semicircular perimeter joined to three straight edges, a circular hole,
|
|
// and a rectangular hole exercise circles-first ShapeBuilder ordering.
|
|
program.MoveTo(0, 0);
|
|
program.LineTo(8, 0);
|
|
program.Codes.Add(new ArcMove(8, 6, 8, 3));
|
|
program.LineTo(0, 6);
|
|
program.LineTo(0, 0);
|
|
program.MoveTo(3, 3);
|
|
program.Codes.Add(new ArcMove(3, 3, 2, 3, RotationType.CW));
|
|
AddRectangle(program, 5, 2, 1, 2, LayerType.Cut);
|
|
return program;
|
|
}
|
|
|
|
// Frozen from PartGeometry at 8664656658d598e55d7bf85c3b392ef6888a593f.
|
|
// These independent preparation bodies and distinct filters intentionally remain
|
|
// duplicated: exact tokens alone did not prove that their overload bindings agree.
|
|
private static List<Line> FrozenGetPartLines(
|
|
Part part, PushDirection facingDirection, double chordTolerance = 0.001)
|
|
{
|
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
|
var shapes = ShapeBuilder.GetShapes(
|
|
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
|
);
|
|
var lines = new List<Line>();
|
|
foreach (var shape in shapes)
|
|
{
|
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
|
polygon.Offset(part.Location);
|
|
lines.AddRange(FrozenDirectionalLines(polygon, facingDirection));
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
private static List<Line> FrozenGetPartLines(
|
|
Part part, Vector facingDirection, double chordTolerance = 0.001)
|
|
{
|
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
|
var shapes = ShapeBuilder.GetShapes(
|
|
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
|
);
|
|
var lines = new List<Line>();
|
|
foreach (var shape in shapes)
|
|
{
|
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
|
polygon.Offset(part.Location);
|
|
lines.AddRange(FrozenDirectionalLines(polygon, facingDirection));
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
private static List<Line> FrozenDirectionalLines(Polygon polygon, Vector direction)
|
|
{
|
|
if (polygon.Vertices.Count < 3)
|
|
return polygon.ToLines();
|
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
|
var lines = new List<Line>();
|
|
var last = polygon.Vertices[0];
|
|
for (var i = 1; i < polygon.Vertices.Count; i++)
|
|
{
|
|
var current = polygon.Vertices[i];
|
|
var edx = current.X - last.X;
|
|
var edy = current.Y - last.Y;
|
|
var keep = sign * (edy * direction.X - edx * direction.Y) > 0;
|
|
if (keep)
|
|
lines.Add(new Line(last, current));
|
|
last = current;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
private static List<Line> FrozenDirectionalLines(Polygon polygon, PushDirection facingDirection)
|
|
{
|
|
if (polygon.Vertices.Count < 3)
|
|
return polygon.ToLines();
|
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
|
var lines = new List<Line>();
|
|
var last = polygon.Vertices[0];
|
|
for (int i = 1; i < polygon.Vertices.Count; i++)
|
|
{
|
|
var current = polygon.Vertices[i];
|
|
var dx = current.X - last.X;
|
|
var dy = current.Y - last.Y;
|
|
bool keep;
|
|
switch (facingDirection)
|
|
{
|
|
case PushDirection.Left:
|
|
keep = -sign * dy > 0;
|
|
break;
|
|
case PushDirection.Right:
|
|
keep = sign * dy > 0;
|
|
break;
|
|
case PushDirection.Up:
|
|
keep = -sign * dx > 0;
|
|
break;
|
|
case PushDirection.Down:
|
|
keep = sign * dx > 0;
|
|
break;
|
|
default:
|
|
keep = true;
|
|
break;
|
|
}
|
|
if (keep)
|
|
lines.Add(new Line(last, current));
|
|
last = current;
|
|
}
|
|
return lines;
|
|
}
|
|
}
|