feat(plateview): annotate overlap centroids
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using System.Globalization;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Diagnostics;
|
||||
|
||||
public class OverlapHoverPagesTests
|
||||
{
|
||||
[Fact]
|
||||
public void CrowdedCoincidentPairsRemainAccessibleInOrderWithinEveryPageBudget()
|
||||
{
|
||||
var parts = Enumerable.Range(1, 12).Select(i => Rectangle($"part-{i}")).ToArray();
|
||||
var report = PlateOverlapAnalyzer.Analyze(parts);
|
||||
var pairs = OverlapPairPresentation.HitTest(report.Pairs.Reverse(), p => p, new Vector(2, 2), 96);
|
||||
var text = string.Join("\n\n", pairs.Select(p => OverlapPairPresentation.Details(p, Units.Millimeters, CultureInfo.InvariantCulture)));
|
||||
var pages = OverlapHoverPages.Create(text, pairs.Count, 10, 45, s => s.Length);
|
||||
|
||||
Assert.True(pages.PageCount > 1);
|
||||
Assert.False(pages.NeedsLargerViewport);
|
||||
var displayed = ReadAll(pages, 10, 45);
|
||||
Assert.Equal(text.Replace("\n", ""), displayed);
|
||||
Assert.Contains("Pair 11/12: part-11 / part-12", displayed);
|
||||
Assert.Contains("Shared area ≈ 16 mm²", displayed);
|
||||
Assert.Contains("Centroid: (2, 2) mm", displayed);
|
||||
Assert.Equal(pages.PageCount - 1, pages.PageIndex);
|
||||
Assert.Contains($"Page {pages.PageCount}/{pages.PageCount}", string.Join("", pages.NavigationLines));
|
||||
Assert.Contains("66 pairs", string.Join("", pages.NavigationLines));
|
||||
Assert.Contains("PgUp/PgDn", string.Join("", pages.NavigationLines));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(8, 12)]
|
||||
[InlineData(4, 40)]
|
||||
public void OneDetailWithLongUnbrokenNamesPagesWithoutDroppingText(int rows, int width)
|
||||
{
|
||||
var name = string.Concat(Enumerable.Repeat("veryLongName😀e\u0301", 30));
|
||||
var pair = Assert.Single(PlateOverlapAnalyzer.Analyze(new[] { Rectangle(name), Rectangle("tail") }).Pairs);
|
||||
var text = OverlapPairPresentation.Details(pair, Units.Inches, CultureInfo.InvariantCulture);
|
||||
var pages = OverlapHoverPages.Create(text, 1, rows, width, s => new StringInfo(s).LengthInTextElements);
|
||||
|
||||
Assert.True(pages.PageCount > 1);
|
||||
Assert.False(pages.NeedsLargerViewport);
|
||||
Assert.Equal(text.Replace("\n", ""), ReadAll(pages, rows, width, s => new StringInfo(s).LengthInTextElements));
|
||||
Assert.All(AllLines(text, rows, width), line =>
|
||||
{
|
||||
Assert.False(line.Length > 0 && char.IsLowSurrogate(line[0]));
|
||||
Assert.False(line.StartsWith("\u0301"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavigationClampsAtBothEndsAndNewHoverStartsAtFirstPage()
|
||||
{
|
||||
const string text = "one\ntwo\nthree\nfour\nfive\nsix";
|
||||
var pages = OverlapHoverPages.Create(text, 2, 4, 40, s => s.Length);
|
||||
Assert.True(pages.PageCount > 1);
|
||||
pages.MovePage(int.MaxValue);
|
||||
Assert.Equal(pages.PageCount - 1, pages.PageIndex);
|
||||
pages.MovePage(1);
|
||||
Assert.Equal(pages.PageCount - 1, pages.PageIndex);
|
||||
pages.MovePage(int.MinValue);
|
||||
Assert.Equal(0, pages.PageIndex);
|
||||
pages.MovePage(-1);
|
||||
Assert.Equal(0, pages.PageIndex);
|
||||
pages.MovePage(1);
|
||||
Assert.Equal(0, OverlapHoverPages.Create(text, 2, 4, 40, s => s.Length).PageIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FittingDetailsDoNotSpendSpaceOnNavigation()
|
||||
{
|
||||
var pages = OverlapHoverPages.Create("Pair 1/2\narea\ncentroid", 1, 3, 40, s => s.Length);
|
||||
Assert.Equal(1, pages.PageCount);
|
||||
Assert.Empty(pages.NavigationLines);
|
||||
Assert.Equal(new[] { "Pair 1/2", "area", "centroid" }, pages.Lines);
|
||||
pages.MovePage(1);
|
||||
Assert.Equal(0, pages.PageIndex);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 40)]
|
||||
[InlineData(3, 0)]
|
||||
[InlineData(1, 5)]
|
||||
[InlineData(4, 0.5)]
|
||||
public void ImpossibleViewportExplicitlyRequestsMoreSpaceInsteadOfDroppingDetails(int rows, double width)
|
||||
{
|
||||
var pages = OverlapHoverPages.Create("Pair 1/2\narea\ncentroid", 1, rows, width, s => s.Length);
|
||||
Assert.True(pages.NeedsLargerViewport);
|
||||
Assert.Empty(pages.Lines);
|
||||
Assert.Equal(0, pages.PageCount);
|
||||
}
|
||||
|
||||
private static string ReadAll(OverlapHoverPages pages, int rows, double width, Func<string, double>? measure = null)
|
||||
{
|
||||
measure ??= s => s.Length;
|
||||
var content = new List<string>();
|
||||
for (var index = 0; index < pages.PageCount; index++)
|
||||
{
|
||||
Assert.Equal(index, pages.PageIndex);
|
||||
Assert.InRange(pages.Lines.Count + pages.NavigationLines.Count, 1, rows);
|
||||
Assert.All(pages.Lines.Concat(pages.NavigationLines), line => Assert.InRange(measure(line), 0, width));
|
||||
content.AddRange(pages.Lines);
|
||||
pages.MovePage(1);
|
||||
}
|
||||
return string.Concat(content);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> AllLines(string text, int rows, int width)
|
||||
{
|
||||
var pages = OverlapHoverPages.Create(text, 1, rows, width, s => new StringInfo(s).LengthInTextElements);
|
||||
for (var i = 0; i < pages.PageCount; i++)
|
||||
{
|
||||
foreach (var line in pages.Lines)
|
||||
yield return line;
|
||||
pages.MovePage(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static Part Rectangle(string name)
|
||||
{
|
||||
var program = new Program(Mode.Absolute);
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(4, 0));
|
||||
program.Codes.Add(new LinearMove(4, 4));
|
||||
program.Codes.Add(new LinearMove(0, 4));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return new Part(new Drawing(name, program));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Globalization;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Diagnostics;
|
||||
|
||||
public class OverlapPairPresentationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Units.Inches, "in")]
|
||||
[InlineData(Units.Millimeters, "mm")]
|
||||
public void DetailsUseCapturedNamesSequenceIdsAndSuppliedUnits(Units units, string symbol)
|
||||
{
|
||||
var parts = new[] { Rectangle("cutoff"), Rectangle("left"), Rectangle("right") };
|
||||
parts[0].BaseDrawing.IsCutOff = true;
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(parts);
|
||||
parts[1].BaseDrawing.Name = "changed";
|
||||
var pair = Assert.Single(PlateOverlapAnalyzer.Analyze(snapshot).Pairs);
|
||||
Assert.Equal("2/3", OverlapPairPresentation.Label(pair));
|
||||
var text = OverlapPairPresentation.Details(pair, units, CultureInfo.InvariantCulture);
|
||||
Assert.Equal($"Pair 2/3: left / right\nShared area ≈ 16 {symbol}²\nCentroid: (2, 2) {symbol}", text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1e-15)]
|
||||
[InlineData(1e-100)]
|
||||
[InlineData(double.Epsilon)]
|
||||
[InlineData(0.000000123456)]
|
||||
public void PositiveTinyValuesNeverFormatAsZero(double value)
|
||||
{
|
||||
var text = OverlapPairPresentation.Number(value, CultureInfo.InvariantCulture);
|
||||
Assert.True(double.Parse(text, CultureInfo.InvariantCulture) > 0, text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(96, 1)]
|
||||
[InlineData(144, 1.5)]
|
||||
[InlineData(192, 2)]
|
||||
public void HitRadiusIsScreenSizedDpiScaledAndInclusive(int dpi, double factor)
|
||||
{
|
||||
Assert.Equal(6 * factor, OverlapPairPresentation.MarkerHalfSize(dpi));
|
||||
Assert.Equal(10 * factor, OverlapPairPresentation.HitRadius(dpi));
|
||||
var pairs = PlateOverlapAnalyzer.Analyze(new[] { Rectangle("a"), Rectangle("b") }).Pairs;
|
||||
foreach (var zoom in new[] { 0.5, 20.0, 500.0 })
|
||||
{
|
||||
Vector ToScreen(Vector world) => new(100 + world.X * zoom, 200 - world.Y * zoom);
|
||||
var center = ToScreen(pairs[0].Centroid);
|
||||
var radius = OverlapPairPresentation.HitRadius(dpi);
|
||||
Assert.Single(OverlapPairPresentation.HitTest(pairs, ToScreen,
|
||||
center + new Vector(radius, 0), dpi));
|
||||
Assert.Empty(OverlapPairPresentation.HitTest(pairs, ToScreen,
|
||||
center + new Vector(radius + 0.01, 0), dpi));
|
||||
Assert.Empty(OverlapPairPresentation.HitTest(pairs, ToScreen,
|
||||
center + new Vector(radius, radius), dpi));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoincidentMarkersReturnEveryPairInIdOrderNotFragmentOrder()
|
||||
{
|
||||
var pairs = PlateOverlapAnalyzer.Analyze(new[] { Rectangle("a"), Rectangle("b"), Rectangle("c") }).Pairs;
|
||||
var hits = OverlapPairPresentation.HitTest(pairs.Reverse(), p => p, new Vector(2, 2), 96);
|
||||
Assert.Equal(new[] { "1/2", "1/3", "2/3" }, hits.Select(OverlapPairPresentation.Label));
|
||||
Assert.All(hits, p => Assert.True(p.Regions.Count > 1));
|
||||
}
|
||||
|
||||
private static Part Rectangle(string name)
|
||||
{
|
||||
var program = new Program(Mode.Absolute);
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(4, 0));
|
||||
program.Codes.Add(new LinearMove(4, 4));
|
||||
program.Codes.Add(new LinearMove(0, 4));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return new Part(new Drawing(name, program));
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,22 @@ public class OverlapReportStateTests
|
||||
Assert.Null(state.Report);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OverlapDisplayMode.Off, OverlapDisplayMode.Areas)]
|
||||
[InlineData(OverlapDisplayMode.Areas, OverlapDisplayMode.Areas)]
|
||||
[InlineData(OverlapDisplayMode.Centroids, OverlapDisplayMode.Centroids)]
|
||||
[InlineData(OverlapDisplayMode.Both, OverlapDisplayMode.Both)]
|
||||
public void CheckAndRecheckPreserveVisibleMode(OverlapDisplayMode chosen, OverlapDisplayMode expected)
|
||||
{
|
||||
var plate = PlateWithParts();
|
||||
var state = new OverlapReportState { DisplayMode = chosen };
|
||||
var request = state.Begin(plate);
|
||||
Assert.Equal(expected, state.DisplayMode);
|
||||
Assert.True(state.TryPublish(request, plate, Analyze(plate)));
|
||||
state.Begin(plate);
|
||||
Assert.Equal(expected, state.DisplayMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewPlateResetsStateButRetainsDocumentDisplayPreference()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public class PlateOverlapAnalyzerTests
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal((0, 1), (pair.PartAId, pair.PartBId));
|
||||
Assert.Equal(0.5, pair.Area, 9);
|
||||
AssertCentroid(pair, 0.75, 0.5);
|
||||
Assert.NotEmpty(pair.Regions);
|
||||
Assert.All(pair.Regions, region =>
|
||||
{
|
||||
@@ -43,6 +44,7 @@ public class PlateOverlapAnalyzerTests
|
||||
Rectangle(0, 0, 4, 4), Rectangle(1, 1, 1, 1)
|
||||
}).Pairs);
|
||||
Assert.Equal(1, pair.Area, 9);
|
||||
AssertCentroid(pair, 1.5, 1.5);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -84,7 +86,9 @@ public class PlateOverlapAnalyzerTests
|
||||
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);
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(0.5, pair.Area, 9);
|
||||
AssertCentroid(pair, 0.75, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -94,7 +98,9 @@ public class PlateOverlapAnalyzerTests
|
||||
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);
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(14, pair.Area, 8);
|
||||
AssertCentroid(pair, 28.5 / 14, 28.5 / 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -109,6 +115,7 @@ public class PlateOverlapAnalyzerTests
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(2, pair.Area, 8);
|
||||
AssertCentroid(pair, 1.5, 2.5);
|
||||
Assert.All(pair.Regions, region => Assert.True(
|
||||
region.Vertices.All(p => p.X <= 1) || region.Vertices.All(p => p.X >= 2)));
|
||||
}
|
||||
@@ -127,6 +134,7 @@ public class PlateOverlapAnalyzerTests
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(0.5, pair.Area, 7);
|
||||
AssertCentroid(pair, x + 0.75, y + 0.5);
|
||||
Assert.Equal(x + 0.5, pair.Bounds.Left, 7);
|
||||
Assert.Equal(y + 1, pair.Bounds.Top, 7);
|
||||
}
|
||||
@@ -141,7 +149,9 @@ public class PlateOverlapAnalyzerTests
|
||||
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);
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(2, pair.Area, 8);
|
||||
AssertCentroid(pair, 9, 9.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -297,6 +307,10 @@ public class PlateOverlapAnalyzerTests
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<Vector>)region.Vertices)[0] = new Vector(99, 99));
|
||||
pair.Bounds.X = 99;
|
||||
Assert.Equal(0, pair.Bounds.X);
|
||||
var centroid = pair.Centroid;
|
||||
centroid.X = 99;
|
||||
centroid.Y = 99;
|
||||
AssertCentroid(pair, 0.5, 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -368,7 +382,9 @@ public class PlateOverlapAnalyzerTests
|
||||
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);
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(1, pair.Area, 8);
|
||||
AssertCentroid(pair, 999999998.5, 999999998.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -423,6 +439,68 @@ public class PlateOverlapAnalyzerTests
|
||||
Assert.Throws<OperationCanceledException>(() => PlateOverlapAnalyzer.Analyze(snapshot, cancellation.Token));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1000000000)]
|
||||
[InlineData(-1000000000)]
|
||||
public void Analyze_UnequalDisconnectedOverlapWeightsAllFragments(double offset)
|
||||
{
|
||||
var u = WithContours(new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(6, 0), new Vector(6, 3), new Vector(3, 3),
|
||||
new Vector(3, 1), new Vector(1, 1), new Vector(1, 3), new Vector(0, 3)
|
||||
});
|
||||
u.Location = new Vector(offset, offset);
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { u, Rectangle(offset, offset + 2, 6, 1) });
|
||||
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(4, pair.Area, 8);
|
||||
AssertCentroid(pair, offset + 3.5, offset + 2.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_RotatedTriangleHasTrueMaterialCentroid()
|
||||
{
|
||||
var triangle = WithContours(new[] { new Vector(0, 0), new Vector(6, 0), new Vector(0, 3) });
|
||||
triangle.Rotate(System.Math.PI / 2);
|
||||
triangle.Location = new Vector(10, 20);
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { Rectangle(0, 0, 30, 30), triangle });
|
||||
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues));
|
||||
var pair = Assert.Single(report.Pairs);
|
||||
Assert.Equal(9, pair.Area, 8);
|
||||
AssertCentroid(pair, 9, 22);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_OverflowingPairMomentsAreIncompleteAndRetainOtherPairs()
|
||||
{
|
||||
var huge = Rectangle(0, 0, 1e103, 1e103);
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[]
|
||||
{
|
||||
huge, new Part(huge.BaseDrawing), Rectangle(-2, -2, 1, 1), Rectangle(-2, -2, 1, 1)
|
||||
});
|
||||
|
||||
Assert.False(report.IsComplete);
|
||||
var issue = Assert.Single(report.Issues);
|
||||
Assert.Equal((0, (int?)1), (issue.PartAId, issue.PartBId));
|
||||
Assert.Contains("moment", issue.Message, StringComparison.OrdinalIgnoreCase);
|
||||
var valid = Assert.Single(report.Pairs);
|
||||
Assert.Equal((2, 3), (valid.PartAId, valid.PartBId));
|
||||
AssertCentroid(valid, -1.5, -1.5);
|
||||
}
|
||||
|
||||
private static void AssertCentroid(PlateOverlapPair pair, double x, double y)
|
||||
{
|
||||
Assert.True(double.IsFinite(pair.Centroid.X));
|
||||
Assert.True(double.IsFinite(pair.Centroid.Y));
|
||||
Assert.Equal(x, pair.Centroid.X, 7);
|
||||
Assert.Equal(y, pair.Centroid.Y, 7);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Geometry;
|
||||
|
||||
public class PolygonAreaMomentsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, false, false)]
|
||||
[InlineData(0, true, false)]
|
||||
[InlineData(0, false, true)]
|
||||
[InlineData(0, true, true)]
|
||||
[InlineData(1000000000, false, false)]
|
||||
[InlineData(1000000000, true, true)]
|
||||
[InlineData(-1000000000, false, true)]
|
||||
[InlineData(-1000000000, true, false)]
|
||||
public void TryCompute_RectangleUsesSignedMomentsWithLocalOrigin(double offset, bool reverse, bool closed)
|
||||
{
|
||||
var vertices = Rectangle(offset + 0.5, offset, 0.5, 1);
|
||||
if (reverse)
|
||||
Array.Reverse(vertices);
|
||||
if (closed)
|
||||
vertices = vertices.Append(vertices[0]).ToArray();
|
||||
|
||||
Assert.True(PolygonAreaMoments.TryCompute(vertices, out var moments));
|
||||
|
||||
Assert.Equal(0.5, moments.Area);
|
||||
Assert.Equal(offset + 0.75, moments.Centroid.X);
|
||||
Assert.Equal(offset + 0.5, moments.Centroid.Y);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void TryCompute_ConcavePolygonUsesAreaNotVertexAverage(bool reverse)
|
||||
{
|
||||
var vertices = new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(3, 0), new Vector(3, 1),
|
||||
new Vector(1, 1), new Vector(1, 3), new Vector(0, 3)
|
||||
};
|
||||
if (reverse)
|
||||
Array.Reverse(vertices);
|
||||
|
||||
Assert.True(PolygonAreaMoments.TryCompute(vertices, out var moments));
|
||||
|
||||
Assert.Equal(5, moments.Area);
|
||||
Assert.Equal(1.1, moments.Centroid.X, 12);
|
||||
Assert.Equal(1.1, moments.Centroid.Y, 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompute_CollinearVerticesDoNotBiasCentroid()
|
||||
{
|
||||
var vertices = new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(1, 0), new Vector(2, 0), new Vector(3, 0),
|
||||
new Vector(3, 2), new Vector(0, 2), new Vector(0, 0)
|
||||
};
|
||||
|
||||
Assert.True(PolygonAreaMoments.TryCompute(vertices, out var moments));
|
||||
|
||||
Assert.Equal(6, moments.Area);
|
||||
Assert.Equal(1.5, moments.Centroid.X);
|
||||
Assert.Equal(1, moments.Centroid.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompute_NearButNotExactClosingVertexIsNotDiscarded()
|
||||
{
|
||||
var vertices = new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(1, 0), new Vector(1, 1),
|
||||
new Vector(0, 1), new Vector(0.000001, 0.000001)
|
||||
};
|
||||
// The final triangle removes area d/2 and has center (d/3, (1+d)/3).
|
||||
const double d = 0.000001;
|
||||
var expectedArea = 1 - d / 2;
|
||||
|
||||
Assert.True(PolygonAreaMoments.TryCompute(vertices, out var moments));
|
||||
|
||||
Assert.Equal(expectedArea, moments.Area, 14);
|
||||
Assert.Equal((0.5 - d * d / 6) / expectedArea, moments.Centroid.X, 14);
|
||||
Assert.Equal((0.5 - d * (1 + d) / 6) / expectedArea, moments.Centroid.Y, 14);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false)]
|
||||
[InlineData(0, true)]
|
||||
[InlineData(1000000000, false)]
|
||||
[InlineData(1000000000, true)]
|
||||
[InlineData(-1000000000, false)]
|
||||
public void TryCombine_UnequalDisconnectedFragmentsUsePositiveAreaWeights(double offset, bool swap)
|
||||
{
|
||||
Assert.True(PolygonAreaMoments.TryCompute(Rectangle(offset, offset, 1, 1), out var small));
|
||||
Assert.True(PolygonAreaMoments.TryCompute(
|
||||
Rectangle(offset + 4, offset, 1, 3).Reverse().ToArray(), out var large));
|
||||
|
||||
Assert.True(PolygonAreaMoments.TryCombine(swap ? new[] { large, small } : new[] { small, large },
|
||||
out var combined));
|
||||
|
||||
Assert.Equal(4, combined.Area);
|
||||
Assert.Equal(offset + 3.5, combined.Centroid.X);
|
||||
Assert.Equal(offset + 1.25, combined.Centroid.Y);
|
||||
// The centroid is in the gap, not on either material fragment.
|
||||
Assert.InRange(combined.Centroid.X - offset, 1.01, 3.99);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> InvalidPolygons()
|
||||
{
|
||||
yield return new object[] { Array.Empty<Vector>() };
|
||||
yield return new object[] { new[] { new Vector(1, 1), new Vector(2, 2) } };
|
||||
yield return new object[] { new[] { new Vector(1, 1), new Vector(2, 2), new Vector(3, 3) } };
|
||||
yield return new object[] { new[] { new Vector(1, 1), new Vector(1, 1), new Vector(1, 1) } };
|
||||
yield return new object[] { new[] { new Vector(0, 0), new Vector(double.NaN, 0), new Vector(0, 1) } };
|
||||
yield return new object[] { new[] { new Vector(0, 0), new Vector(1, double.PositiveInfinity), new Vector(0, 1) } };
|
||||
yield return new object[] { new[] { new Vector(double.NegativeInfinity, 0), new Vector(1, 0), new Vector(0, 1) } };
|
||||
yield return new object[] { Rectangle(0, 0, 1e200, 1e200) };
|
||||
yield return new object[] { Rectangle(0, 0, 1e103, 1e103) };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidPolygons))]
|
||||
public void TryCompute_DegenerateNonfiniteOrOverflowingMomentsAreRejected(Vector[] vertices)
|
||||
{
|
||||
Assert.False(PolygonAreaMoments.TryCompute(vertices, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCombine_EmptyOrInvalidFragmentRejectsWholeResult()
|
||||
{
|
||||
Assert.True(PolygonAreaMoments.TryCompute(Rectangle(1, 1, 1, 1), out var valid));
|
||||
|
||||
Assert.False(PolygonAreaMoments.TryCombine(Array.Empty<PolygonAreaMoments>(), out _));
|
||||
Assert.False(PolygonAreaMoments.TryCombine(new[] { valid, default(PolygonAreaMoments) }, out _));
|
||||
Assert.False(PolygonAreaMoments.TryCombine(new[] { default(PolygonAreaMoments), valid }, out _));
|
||||
}
|
||||
|
||||
private static Vector[] Rectangle(double x, double y, double width, double height) =>
|
||||
new[] { new Vector(x, y), new Vector(x + width, y),
|
||||
new Vector(x + width, y + height), new Vector(x, y + height) };
|
||||
}
|
||||
Reference in New Issue
Block a user