merge: integrate polygon overlap diagnostics
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Diagnostics;
|
||||
|
||||
public class PlateOverlapAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Analyze_ReturnsOwnedWorldPolygonsForEachOverlappingPair()
|
||||
{
|
||||
var parts = new[] { Rectangle(0, 0, 1, 1), Rectangle(0.5, 0, 1, 1) };
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(parts);
|
||||
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Issues);
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal((0, 1), (pair.PartAId, pair.PartBId));
|
||||
Assert.Equal(0.5, pair.Area, 9);
|
||||
Assert.NotEmpty(pair.Regions);
|
||||
Assert.All(pair.Regions, region =>
|
||||
{
|
||||
Assert.True(region.Area > 0);
|
||||
Assert.Equal(region.Vertices[0], region.Vertices[^1]);
|
||||
Assert.All(region.Vertices, point =>
|
||||
{
|
||||
Assert.InRange(point.X, 0.5, 1);
|
||||
Assert.InRange(point.Y, 0, 1);
|
||||
});
|
||||
});
|
||||
Assert.Equal(pair.Area, pair.Regions.Sum(r => r.Area), 9);
|
||||
Assert.Equal((0.5, 0.0, 1.0, 1.0),
|
||||
(pair.Bounds.Left, pair.Bounds.Bottom, pair.Bounds.Right, pair.Bounds.Top));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_FullContainmentDoesNotRequireCrossingPoints()
|
||||
{
|
||||
var pair = Assert.Single(PlateOverlapAnalyzer.Analyze(new[]
|
||||
{
|
||||
Rectangle(0, 0, 4, 4), Rectangle(1, 1, 1, 1)
|
||||
}).Pairs);
|
||||
Assert.Equal(1, pair.Area, 9);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 0)]
|
||||
[InlineData(1, 0)]
|
||||
[InlineData(1, 1)]
|
||||
public void Analyze_DisjointAndBoundaryOnlyContactAreClear(double x, double y)
|
||||
{
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[]
|
||||
{
|
||||
Rectangle(0, 0, 1, 1), Rectangle(x, y, 1, 1)
|
||||
});
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_EmptyAndSinglePartAreComplete()
|
||||
{
|
||||
foreach (var parts in new[] { Array.Empty<Part>(), new[] { Rectangle(0, 0, 1, 1) } })
|
||||
{
|
||||
var report = PlateOverlapAnalyzer.Analyze(parts);
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Analyze_SubtractsHolesOfEitherOperand(bool swap)
|
||||
{
|
||||
var frame = WithContours(Square(0, 0, 4), Square(1, 1, 2));
|
||||
var insert = Rectangle(1.5, 1.5, 1, 1);
|
||||
var report = PlateOverlapAnalyzer.Analyze(swap ? new[] { insert, frame } : new[] { frame, insert });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Pairs);
|
||||
|
||||
insert.Location = new Vector(0.5, 1.5);
|
||||
report = PlateOverlapAnalyzer.Analyze(swap ? new[] { insert, frame } : new[] { frame, insert });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(0.5, Assert.Single(report.Pairs).Area, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SubtractsBothPartsHolesWithoutDuplicatingArea()
|
||||
{
|
||||
var a = WithContours(Square(0, 0, 4), Square(0.5, 0.5, 1));
|
||||
var b = WithContours(Square(0, 0, 4), Square(2, 2, 1));
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { a, b });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(14, Assert.Single(report.Pairs).Area, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_ConcaveIntersectionKeepsDisconnectedFragments()
|
||||
{
|
||||
var u = WithContours(new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(3, 0), new Vector(3, 3), new Vector(2, 3),
|
||||
new Vector(2, 1), new Vector(1, 1), new Vector(1, 3), new Vector(0, 3)
|
||||
});
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { u, Rectangle(0, 2, 3, 1) });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(2, pair.Area, 8);
|
||||
Assert.All(pair.Regions, region => Assert.True(
|
||||
region.Vertices.All(p => p.X <= 1) || region.Vertices.All(p => p.X >= 2)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(1000000000, 1000000000)]
|
||||
[InlineData(-1000000000, -1000000000)]
|
||||
public void Analyze_ReversedWindingAndLargeTranslationPreserveArea(double x, double y)
|
||||
{
|
||||
var a = WithContours(Square(0, 0, 1).Reverse().ToArray());
|
||||
var b = Rectangle(0, 0, 1, 1);
|
||||
a.Location = new Vector(x, y);
|
||||
b.Location = new Vector(x + 0.5, y);
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { a, b });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(0.5, pair.Area, 7);
|
||||
Assert.Equal(x + 0.5, pair.Bounds.Left, 7);
|
||||
Assert.Equal(y + 1, pair.Bounds.Top, 7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_UsesBaselineAdjustedCleanDrawingPose()
|
||||
{
|
||||
var drawing = Rectangle(0, 0, 2, 1).BaseDrawing;
|
||||
drawing.Program.Rotate(System.Math.PI / 2);
|
||||
var part = new Part(drawing);
|
||||
part.Rotate(System.Math.PI / 2);
|
||||
part.Location = new Vector(10, 10);
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { part, Rectangle(8, 9, 2, 1) });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(2, Assert.Single(report.Pairs).Area, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_CircularHoleIsNotSolidMaterial()
|
||||
{
|
||||
var ring = new Part(new RingShape { OuterDiameter = 10, InnerDiameter = 7 }.GetDrawing());
|
||||
var disk = new Part(new CircleShape { Diameter = 6 }.GetDrawing());
|
||||
ring.Location = new Vector(-36.8, 5.4);
|
||||
disk.Location = new Vector(-36.7, 5.4);
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { ring, disk });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_KeepsInputIndicesAndDeterministicOrderWhileSkippingCutoffs()
|
||||
{
|
||||
var cutoff = Rectangle(0, 0, 1, 1);
|
||||
cutoff.BaseDrawing.IsCutOff = true;
|
||||
var drawing = Rectangle(0, 0, 1, 1).BaseDrawing;
|
||||
var parts = new[] { new Part(drawing), cutoff, new Part(drawing), new Part(drawing) };
|
||||
var report = PlateOverlapAnalyzer.Analyze(parts);
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(new[] { (0, 2), (0, 3), (2, 3) },
|
||||
report.Pairs.Select(p => (p.PartAId, p.PartBId)));
|
||||
Assert.All(report.Pairs, p => Assert.Equal(1, p.Area, 9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SweepMatchesAnalyticalExhaustiveRectanglePairs()
|
||||
{
|
||||
var random = new Random(928);
|
||||
var parts = Enumerable.Range(0, 40)
|
||||
.Select(_ => Rectangle(random.Next(-10, 10), random.Next(-10, 10), 3, 2)).ToArray();
|
||||
var expected = new List<(int, int, double)>();
|
||||
for (var a = 0; a < parts.Length; a++)
|
||||
for (var b = a + 1; b < parts.Length; b++)
|
||||
{
|
||||
var dx = System.Math.Min(parts[a].Right, parts[b].Right)
|
||||
- System.Math.Max(parts[a].Left, parts[b].Left);
|
||||
var dy = System.Math.Min(parts[a].Top, parts[b].Top)
|
||||
- System.Math.Max(parts[a].Bottom, parts[b].Bottom);
|
||||
if (dx > 0 && dy > 0)
|
||||
expected.Add((a, b, dx * dy));
|
||||
}
|
||||
var report = PlateOverlapAnalyzer.Analyze(parts);
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(expected.Select(pair => (pair.Item1, pair.Item2)),
|
||||
report.Pairs.Select(pair => (pair.PartAId, pair.PartBId)));
|
||||
foreach (var (expectedPair, actualPair) in expected.Zip(report.Pairs))
|
||||
Assert.Equal(expectedPair.Item3, actualPair.Area, 9);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("open")]
|
||||
[InlineData("empty")]
|
||||
[InlineData("nan")]
|
||||
[InlineData("infinite-pose")]
|
||||
[InlineData("disjoint-outers")]
|
||||
[InlineData("nested-island")]
|
||||
[InlineData("crossing-holes")]
|
||||
[InlineData("self-crossing")]
|
||||
public void Analyze_InvalidPartMakesReportIncompleteButRetainsKnownOverlaps(string invalid)
|
||||
{
|
||||
var bad = Rectangle(0, 0, 1, 1);
|
||||
switch (invalid)
|
||||
{
|
||||
case "open": bad.BaseDrawing.Program.Codes.RemoveAt(bad.BaseDrawing.Program.Codes.Count - 1); break;
|
||||
case "empty": bad.BaseDrawing.Program.Codes.Clear(); break;
|
||||
case "nan": ((LinearMove)bad.BaseDrawing.Program.Codes[1]).EndPoint = new Vector(double.NaN, 0); break;
|
||||
case "infinite-pose": bad.Location = new Vector(double.PositiveInfinity, 0); break;
|
||||
case "disjoint-outers": bad = WithContours(Square(0, 0, 1), Square(3, 3, 1)); break;
|
||||
case "nested-island": bad = WithContours(Square(0, 0, 6), Square(1, 1, 4), Square(2, 2, 1)); break;
|
||||
case "crossing-holes": bad = WithContours(Square(0, 0, 6), Square(1, 1, 3), Square(2, 2, 3)); break;
|
||||
case "self-crossing": bad = WithContours(new[] { new Vector(0, 0), new Vector(3, 3), new Vector(0, 3), new Vector(2, 0) }); break;
|
||||
}
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { Rectangle(0, 0, 1, 1), bad, Rectangle(0, 0, 1, 1) });
|
||||
Assert.False(report.IsComplete);
|
||||
var issue = Assert.Single(report.Issues);
|
||||
Assert.Equal(1, issue.PartAId);
|
||||
Assert.Null(issue.PartBId);
|
||||
Assert.False(string.IsNullOrWhiteSpace(issue.Message));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal((0, 2), (pair.PartAId, pair.PartBId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_IsIndependentOfLaterDrawingPoseAndCollectionChanges()
|
||||
{
|
||||
var a = Rectangle(0, 0, 1, 1);
|
||||
var b = new Part(a.BaseDrawing, new Vector(0.5, 0));
|
||||
var parts = new List<Part> { a, b };
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(parts);
|
||||
a.BaseDrawing.Program.Codes.Clear();
|
||||
b.Location = new Vector(100, 100);
|
||||
parts.Clear();
|
||||
var first = PlateOverlapAnalyzer.Analyze(snapshot);
|
||||
var second = PlateOverlapAnalyzer.Analyze(snapshot);
|
||||
Assert.True(first.IsComplete);
|
||||
Assert.Equal(0.5, Assert.Single(first.Pairs).Area, 9);
|
||||
Assert.Equal(0.5, Assert.Single(second.Pairs).Area, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_IgnoresPlacedCuttingProgramAndDoesNotMutateParts()
|
||||
{
|
||||
var a = Rectangle(0, 0, 1, 1);
|
||||
var b = Rectangle(0.5, 0, 1, 1);
|
||||
Assert.True(a.RestoreLeadInProgram(Rectangle(100, 100, 4, 4).Program, locked: true));
|
||||
var program = a.Program;
|
||||
var source = a.BaseDrawing.Program;
|
||||
var sourceText = source.ToString();
|
||||
var placedText = program.ToString();
|
||||
var bounds = a.BoundingBox;
|
||||
var location = a.Location;
|
||||
var rotation = a.Rotation;
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { a, b });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(0.5, Assert.Single(report.Pairs).Area, 9);
|
||||
Assert.Same(program, a.Program);
|
||||
Assert.Same(source, a.BaseDrawing.Program);
|
||||
Assert.Equal(sourceText, source.ToString());
|
||||
Assert.Equal(placedText, program.ToString());
|
||||
Assert.Same(bounds, a.BoundingBox);
|
||||
Assert.Equal(location, a.Location);
|
||||
Assert.Equal(rotation, a.Rotation);
|
||||
Assert.True(a.HasManualLeadIns);
|
||||
Assert.True(a.LeadInsLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_IgnoresScribeRapidAndLeadPathsInCleanSource()
|
||||
{
|
||||
var a = Rectangle(0, 0, 1, 1);
|
||||
foreach (var layer in new[] { LayerType.Scribe, LayerType.Leadin, LayerType.Leadout })
|
||||
{
|
||||
a.BaseDrawing.Program.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
a.BaseDrawing.Program.Codes.Add(new LinearMove(new Vector(10, 10)) { Layer = layer });
|
||||
}
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { a, Rectangle(5, 5, 1, 1) });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_ReportCollectionsAndBoundsCannotBeMutated()
|
||||
{
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { Rectangle(0, 0, 1, 1), Rectangle(0, 0, 1, 1) });
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<PlateOverlapPair>)report.Pairs).Clear());
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<PlateOverlapRegion>)pair.Regions).Clear());
|
||||
var region = pair.Regions[0];
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<Vector>)region.Vertices)[0] = new Vector(99, 99));
|
||||
pair.Bounds.X = 99;
|
||||
Assert.Equal(0, pair.Bounds.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Capture_SharedHoleSubprogramsAreOwnedAndReusableAcrossWorkers()
|
||||
{
|
||||
var source = Rectangle(0, 0, 10, 10).BaseDrawing;
|
||||
var hole = Rectangle(0, 0, 1, 1).BaseDrawing.Program;
|
||||
hole.Mode = Mode.Incremental;
|
||||
source.Program.SubPrograms[-1] = hole;
|
||||
source.Program.Codes.Add(new SubProgramCall { Id = -1, Program = hole, Offset = new Vector(2, 2) });
|
||||
source.Program.Codes.Add(new SubProgramCall { Id = -1, Program = hole, Offset = new Vector(6, 2) });
|
||||
source.Program.Rotate(System.Math.PI / 2);
|
||||
var placed = new Part(source);
|
||||
placed.Rotate(System.Math.PI / 2);
|
||||
placed.Location = new Vector(20, 20);
|
||||
var parts = new[] { placed, Rectangle(10, 10, 10, 10) };
|
||||
var before = source.Program.Codes.Select(code => code.ToString()).ToArray();
|
||||
var holeBefore = hole.Codes.Select(code => code.ToString()).ToArray();
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(parts);
|
||||
Assert.Equal(before, source.Program.Codes.Select(code => code.ToString()));
|
||||
Assert.Equal(holeBefore, hole.Codes.Select(code => code.ToString()));
|
||||
foreach (var call in source.Program.Codes.OfType<SubProgramCall>())
|
||||
call.Offset = new Vector(100, 100);
|
||||
hole.Codes.Clear();
|
||||
source.Program.Codes.Clear();
|
||||
var reports = await Task.WhenAll(Enumerable.Range(0, 4).Select(_ =>
|
||||
Task.Run(() => PlateOverlapAnalyzer.Analyze(snapshot))));
|
||||
foreach (var report in reports)
|
||||
{
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(98, Assert.Single(report.Pairs).Area, 8);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Capture_InvalidSubprogramGraphIsIncompleteRatherThanRecursive(bool missing)
|
||||
{
|
||||
var part = Rectangle(0, 0, 1, 1);
|
||||
var call = new SubProgramCall();
|
||||
if (!missing)
|
||||
call.Program = part.BaseDrawing.Program;
|
||||
part.BaseDrawing.Program.Codes.Add(call);
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { part });
|
||||
Assert.False(report.IsComplete);
|
||||
Assert.Single(report.Issues);
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_NullArgumentsThrowAndNullEntriesAreIssues()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => PlateOverlapAnalyzer.Capture(null!));
|
||||
Assert.Throws<ArgumentNullException>(() => PlateOverlapAnalyzer.Analyze((PlateOverlapSnapshot)null!));
|
||||
var report = PlateOverlapAnalyzer.Analyze(new Part[] { null! });
|
||||
Assert.False(report.IsComplete);
|
||||
Assert.Single(report.Issues);
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Analyze_HugeOuterWithSmallClockwisePartIsOrderIndependent(bool swap)
|
||||
{
|
||||
var outer = Rectangle(0, 0, 1e9, 1e9);
|
||||
var small = WithContours(Square(0, 0, 1).Reverse().ToArray());
|
||||
small.Location = new Vector(999999998, 999999998);
|
||||
var report = PlateOverlapAnalyzer.Analyze(swap ? new[] { small, outer } : new[] { outer, small });
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
Assert.Equal(1, Assert.Single(report.Pairs).Area, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_TranslationThatCollapsesMaterialIsIncompleteNotClear()
|
||||
{
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[]
|
||||
{
|
||||
Rectangle(1e16, 1e16, 1, 1), Rectangle(1e16, 1e16, 1, 1)
|
||||
});
|
||||
Assert.False(report.IsComplete);
|
||||
Assert.Equal(new[] { 0, 1 }, report.Issues.Select(issue => issue.PartAId));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(9.0)]
|
||||
[InlineData(9.0002)]
|
||||
public void Analyze_NativeCircularHoleContactCannotBeHiddenByTessellation(double y)
|
||||
{
|
||||
var source = Rectangle(0, 0, 10, 10).BaseDrawing;
|
||||
source.Program.Codes.Add(new RapidMove(new Vector(6, y)));
|
||||
source.Program.Codes.Add(new ArcMove(new Vector(6, y), new Vector(5, y), RotationType.CCW));
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { new Part(source) });
|
||||
Assert.False(report.IsComplete);
|
||||
Assert.Single(report.Issues);
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SubChordThinRingIsExplicitlyUncheckableNotClear()
|
||||
{
|
||||
const double radius = 4.995098381203606;
|
||||
var ring = new Part(new RingShape
|
||||
{
|
||||
OuterDiameter = 2 * (radius + 0.0001),
|
||||
InnerDiameter = 2 * (radius - 0.0001)
|
||||
}.GetDrawing());
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { ring });
|
||||
Assert.False(report.IsComplete);
|
||||
Assert.Single(report.Issues);
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_CancellationThrowsInsteadOfPublishingPartialClear()
|
||||
{
|
||||
var parts = new[] { Rectangle(0, 0, 1, 1), Rectangle(0, 0, 1, 1) };
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(parts);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
Assert.Throws<OperationCanceledException>(() => PlateOverlapAnalyzer.Capture(parts, cancellation.Token));
|
||||
Assert.Throws<OperationCanceledException>(() => PlateOverlapAnalyzer.Analyze(snapshot, cancellation.Token));
|
||||
}
|
||||
|
||||
private static Part Rectangle(double x, double y, double width, double height) =>
|
||||
new(WithContours(new[] { new Vector(0, 0), new Vector(width, 0),
|
||||
new Vector(width, height), new Vector(0, height) }).BaseDrawing, new Vector(x, y));
|
||||
|
||||
private static Vector[] Square(double x, double y, double size) =>
|
||||
new[] { new Vector(x, y), new Vector(x + size, y), new Vector(x + size, y + size), new Vector(x, y + size) };
|
||||
|
||||
private static Part WithContours(params Vector[][] contours)
|
||||
{
|
||||
var program = new Program(Mode.Absolute);
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
program.Codes.Add(new RapidMove(contour[0]));
|
||||
foreach (var point in contour.Skip(1).Append(contour[0]))
|
||||
program.Codes.Add(new LinearMove(point));
|
||||
}
|
||||
return new Part(new Drawing("same name", program));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Geometry;
|
||||
|
||||
public class CollisionTranslationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Check_ContainedClockwisePolygonFarFromOriginDoesNotDependOnOperandOrder(bool swap)
|
||||
{
|
||||
var outer = Square(0, 0, 1e9);
|
||||
var small = Square(999999998, 999999998, 1);
|
||||
small.Reverse();
|
||||
|
||||
var result = swap ? Collision.Check(small, outer) : Collision.Check(outer, small);
|
||||
|
||||
Assert.True(result.Overlaps);
|
||||
// Do not use Polygon.Area here: this test isolates triangulation, not that legacy API.
|
||||
Assert.Equal(1, result.OverlapRegions.Sum(StableArea), 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Check_ClockwiseHoleFarFromOriginIsSubtracted()
|
||||
{
|
||||
var outer = Square(0, 0, 1e9);
|
||||
var hole = Square(999999998, 999999998, 1);
|
||||
hole.Reverse();
|
||||
var insert = Square(999999998.25, 999999998.25, 0.5);
|
||||
|
||||
var result = Collision.Check(outer, insert, new List<Polygon> { hole });
|
||||
|
||||
Assert.False(result.Overlaps);
|
||||
Assert.Empty(result.OverlapRegions);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1000000000)]
|
||||
[InlineData(-1000000000)]
|
||||
public void Triangulate_ClockwiseTranslatedUnitSquareHasFullArea(double offset)
|
||||
{
|
||||
var polygon = Square(offset, offset, 1);
|
||||
polygon.Reverse();
|
||||
var triangles = ConvexDecomposition.Triangulate(polygon);
|
||||
Assert.Equal(2, triangles.Count);
|
||||
Assert.Equal(1, triangles.Sum(StableArea), 8);
|
||||
}
|
||||
|
||||
private static Polygon Square(double x, double y, double size)
|
||||
{
|
||||
var polygon = new Polygon();
|
||||
polygon.Vertices.AddRange(new[] { new Vector(x, y), new Vector(x + size, y),
|
||||
new Vector(x + size, y + size), new Vector(x, y + size), new Vector(x, y) });
|
||||
polygon.UpdateBounds();
|
||||
return polygon;
|
||||
}
|
||||
|
||||
private static double StableArea(Polygon polygon)
|
||||
{
|
||||
var area = 0.0;
|
||||
var origin = polygon.Vertices[0];
|
||||
for (var i = 1; i + 1 < polygon.Vertices.Count; i++)
|
||||
{
|
||||
var a = polygon.Vertices[i] - origin;
|
||||
var b = polygon.Vertices[i + 1] - origin;
|
||||
area += a.X * b.Y - a.Y * b.X;
|
||||
}
|
||||
return System.Math.Abs(area) / 2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user