mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-17 15:01:41 -04:00
122 lines
5.0 KiB
C#
122 lines
5.0 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Engine.Fill;
|
|
using OpenNest.Geometry;
|
|
using Xunit;
|
|
|
|
namespace OpenNest.Engine.Tests.Fill;
|
|
|
|
public class ExpanderCutoffDeltaTests
|
|
{
|
|
private static Plate Sheet() => new(new Size(30, 30)) { EdgeSpacing = new Spacing(0, 0) };
|
|
|
|
private static Program Rectangle(double width = 2, double height = 2)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(0, 0);
|
|
program.LineTo(width, 0);
|
|
program.LineTo(width, height);
|
|
program.LineTo(0, height);
|
|
program.LineTo(0, 0);
|
|
return program;
|
|
}
|
|
|
|
private static Part Add(Plate plate, Program program, double x, double y)
|
|
{
|
|
var part = new Part(new Drawing("neutral", program), new Vector(x, y));
|
|
plate.Parts.Add(part);
|
|
return part;
|
|
}
|
|
|
|
private static Program Disk(double radius) =>
|
|
ConvertGeometry.ToProgram(new List<Entity> { new Circle(0, 0, radius) });
|
|
|
|
[Theory]
|
|
[InlineData(CutOffAxis.Horizontal)]
|
|
[InlineData(CutOffAxis.Vertical)]
|
|
public void Expand_RegeneratedCutoff_MustPreserveNativeEntryGap(CutOffAxis axis)
|
|
{
|
|
var plate = Sheet();
|
|
var anchor = Add(plate, Rectangle(), 1, 1);
|
|
var mover = Add(plate, Rectangle(), 4, 4);
|
|
plate.CutOffs.Add(new CutOff(new Vector(10, 10), axis));
|
|
var settings = new CutOffSettings();
|
|
plate.RegenerateCutOffs(settings);
|
|
var before = axis == CutOffAxis.Horizontal
|
|
? 10 - mover.BoundingBox.Top : 10 - mover.BoundingBox.Right;
|
|
|
|
var result = Expander.Expand(new List<Part> { anchor, mover }, plate,
|
|
new Expander.Options { MaxSpacing = 3 });
|
|
var after = axis == CutOffAxis.Horizontal
|
|
? 10 - mover.BoundingBox.Top : 10 - mover.BoundingBox.Right;
|
|
Assert.True(after >= before - 1.1e-6,
|
|
$"Cutoff entry gap shrank {before:R}->{after:R}, mover=({mover.Location.X:R},{mover.Location.Y:R}), achieved={result.AchievedSpacing:R}, violations={result.Violations.Count}");
|
|
plate.RegenerateCutOffs(settings);
|
|
Assert.Single(plate.CutOffs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Expand_RemoteOpenU_MustNotMoveTowardNativeCutoff()
|
|
{
|
|
var plate = Sheet();
|
|
var anchor = Add(plate, Rectangle(), 1, 4);
|
|
var mover = Add(plate, Rectangle(), 4, 4);
|
|
var program = new Program();
|
|
program.MoveTo(0, 10);
|
|
program.LineTo(30, 10);
|
|
program.LineTo(30, 0);
|
|
program.LineTo(0, 0);
|
|
plate.Parts.Add(new Part(new Drawing("neutral-cutoff", program) { IsCutOff = true }, Vector.Zero));
|
|
|
|
var result = Expander.Expand(new List<Part> { anchor, mover }, plate,
|
|
new Expander.Options { MaxSpacing = 3 });
|
|
// The three real segments are at least four units from the selected
|
|
// square at entry; an invented closing chord is not a real obstacle.
|
|
var gap = System.Math.Min(mover.BoundingBox.Bottom,
|
|
System.Math.Min(10 - mover.BoundingBox.Top, 30 - mover.BoundingBox.Right));
|
|
Assert.True(gap >= 4 - 1.1e-6,
|
|
$"Open path gap shrank to {gap:R}; mover=({mover.Location.X:R},{mover.Location.Y:R}); spacing={result.AchievedSpacing:R}, violations={result.Violations.Count}");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void Separate_ShallowDiskOrRingVsRectangle_MustNotClaimClear(bool annulus)
|
|
{
|
|
var plate = Sheet();
|
|
var entities = new List<Entity> { new Circle(0, 0, 2) };
|
|
if (annulus)
|
|
entities.Add(new Circle(0, 0, 1));
|
|
Add(plate, ConvertGeometry.ToProgram(entities), 10, 10);
|
|
const double angle = 0.017;
|
|
const double depth = 0.0005;
|
|
var mover = Add(plate, Rectangle(0.01, 0.01),
|
|
10 + (2 - depth) * System.Math.Cos(angle),
|
|
10 + (2 - depth) * System.Math.Sin(angle));
|
|
|
|
var result = Expander.Separate(new List<Part> { mover }, plate, 0);
|
|
var nearest = result.Positions[0].DistanceTo(new Vector(10, 10));
|
|
Assert.True(nearest >= 2 - 1e-8 || result.Violations.Any(v => v.Achieved < 0),
|
|
$"Native material overlap remains: nearest={nearest:R}, converged={result.Converged}, violations={result.Violations.Count}");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(CutOffAxis.Horizontal)]
|
|
[InlineData(CutOffAxis.Vertical)]
|
|
public void Separate_RegeneratedCutoff_MustNotCrossEvenAtFarClearEndpoint(CutOffAxis axis)
|
|
{
|
|
var plate = Sheet();
|
|
var horizontal = axis == CutOffAxis.Horizontal;
|
|
var anchor = Add(plate, Rectangle(), 1, 1);
|
|
var mover = Add(plate, Rectangle(), horizontal ? 1 : 6, horizontal ? 6 : 1);
|
|
plate.CutOffs.Add(new CutOff(new Vector(10, 10), axis));
|
|
plate.RegenerateCutOffs(new CutOffSettings());
|
|
|
|
var result = Expander.Separate(new List<Part> { anchor, mover }, plate, 12);
|
|
var trailingEdge = (horizontal ? result.Positions[1].Y : result.Positions[1].X) + 2;
|
|
Assert.True(trailingEdge <= 10 + 1e-6,
|
|
$"Crossed native cutoff, trailing edge={trailingEdge:R}");
|
|
Assert.False(result.Converged);
|
|
}
|
|
}
|