mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 06:51:35 -05:00
A drawing of two equal, separate outlines (RC-204441A23 PT13) froze the drawing library preview. ShapeProfile takes one outline as the perimeter and the other as a cutout, so the net area is rounding noise and ThicknessPrecision fell to 1e-9. A rectangle's pole is a whole ridge, so the label search refined it into billions of cells on the UI thread. Net area negligible next to the outline now falls back to the outline's own width, and the paint-time label search has a cell budget so no shape can stall painting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
6.6 KiB
C#
196 lines
6.6 KiB
C#
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.Geometry;
|
|
|
|
public class PolyLabelTests
|
|
{
|
|
private static Polygon Square(double size)
|
|
{
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(size, 0));
|
|
p.Vertices.Add(new Vector(size, size));
|
|
p.Vertices.Add(new Vector(0, size));
|
|
return p;
|
|
}
|
|
|
|
[Fact]
|
|
public void Square_ReturnsCenterPoint()
|
|
{
|
|
var poly = Square(100);
|
|
|
|
var result = PolyLabel.Find(poly);
|
|
|
|
Assert.Equal(50, result.X, 1.0);
|
|
Assert.Equal(50, result.Y, 1.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Triangle_ReturnsIncenter()
|
|
{
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(100, 0));
|
|
p.Vertices.Add(new Vector(50, 86.6));
|
|
|
|
var result = PolyLabel.Find(p);
|
|
|
|
// Incenter of equilateral triangle is at (50, ~28.9)
|
|
Assert.Equal(50, result.X, 1.0);
|
|
Assert.Equal(28.9, result.Y, 1.0);
|
|
Assert.True(p.ContainsPoint(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void LShape_ReturnsPointInBottomLobe()
|
|
{
|
|
// L-shape: 100x100 with 50x50 cut from top-right
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(100, 0));
|
|
p.Vertices.Add(new Vector(100, 50));
|
|
p.Vertices.Add(new Vector(50, 50));
|
|
p.Vertices.Add(new Vector(50, 100));
|
|
p.Vertices.Add(new Vector(0, 100));
|
|
|
|
var result = PolyLabel.Find(p);
|
|
|
|
Assert.True(p.ContainsPoint(result));
|
|
// The bottom 100x50 lobe is the widest region
|
|
Assert.True(result.Y < 50, $"Expected label in bottom lobe, got Y={result.Y}");
|
|
}
|
|
|
|
[Fact]
|
|
public void ThinRectangle_CenteredOnBothAxes()
|
|
{
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(200, 0));
|
|
p.Vertices.Add(new Vector(200, 10));
|
|
p.Vertices.Add(new Vector(0, 10));
|
|
|
|
var result = PolyLabel.Find(p);
|
|
|
|
Assert.Equal(100, result.X, 1.0);
|
|
Assert.Equal(5, result.Y, 1.0);
|
|
Assert.True(p.ContainsPoint(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void SquareWithLargeHole_AvoidsHole()
|
|
{
|
|
var outer = Square(100);
|
|
|
|
var hole = new Polygon();
|
|
hole.Vertices.Add(new Vector(20, 20));
|
|
hole.Vertices.Add(new Vector(80, 20));
|
|
hole.Vertices.Add(new Vector(80, 80));
|
|
hole.Vertices.Add(new Vector(20, 80));
|
|
|
|
var result = PolyLabel.Find(outer, new[] { hole });
|
|
|
|
// Point should be inside outer but outside hole
|
|
Assert.True(outer.ContainsPoint(result));
|
|
Assert.False(hole.ContainsPoint(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void CShape_ReturnsPointInLeftBar()
|
|
{
|
|
// C-shape opening to the right: left bar is 20 wide, top/bottom arms are 20 tall
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(100, 0));
|
|
p.Vertices.Add(new Vector(100, 20));
|
|
p.Vertices.Add(new Vector(20, 20));
|
|
p.Vertices.Add(new Vector(20, 80));
|
|
p.Vertices.Add(new Vector(100, 80));
|
|
p.Vertices.Add(new Vector(100, 100));
|
|
p.Vertices.Add(new Vector(0, 100));
|
|
|
|
var result = PolyLabel.Find(p);
|
|
|
|
Assert.True(p.ContainsPoint(result));
|
|
// Label should be in the left vertical bar (x < 20), not at bbox center (50, 50)
|
|
Assert.True(result.X < 20, $"Expected label in left bar, got X={result.X}");
|
|
}
|
|
|
|
[Fact]
|
|
public void ThicknessPrecision_FindsThickBarOfSmallPart()
|
|
{
|
|
// 1.5-unit C-shape: left bar 0.6 wide, arms 0.15 tall. A fixed 0.5 precision is a
|
|
// third of the part and stops on the first interior cell.
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(0, 0));
|
|
p.Vertices.Add(new Vector(1.5, 0));
|
|
p.Vertices.Add(new Vector(1.5, 0.15));
|
|
p.Vertices.Add(new Vector(0.6, 0.15));
|
|
p.Vertices.Add(new Vector(0.6, 1.35));
|
|
p.Vertices.Add(new Vector(1.5, 1.35));
|
|
p.Vertices.Add(new Vector(1.5, 1.5));
|
|
p.Vertices.Add(new Vector(0, 1.5));
|
|
|
|
var result = PolyLabel.Find(p, null, PolyLabel.ThicknessPrecision(p));
|
|
|
|
// Any height along the bar is equally clear; its centerline is not.
|
|
Assert.Equal(0.3, result.X, 0.02);
|
|
Assert.InRange(result.Y, 0.3, 1.2);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThicknessPrecision_ScalesWithMaterialWidth()
|
|
{
|
|
var hole = new Polygon();
|
|
hole.Vertices.Add(new Vector(20, 20));
|
|
hole.Vertices.Add(new Vector(80, 20));
|
|
hole.Vertices.Add(new Vector(80, 80));
|
|
hole.Vertices.Add(new Vector(20, 80));
|
|
|
|
// Square side s: 2 * s^2 / 4s = s / 2.
|
|
Assert.Equal(0.05 * 50, PolyLabel.ThicknessPrecision(Square(100)), 9);
|
|
Assert.Equal(0.05 * 0.5, PolyLabel.ThicknessPrecision(Square(1)), 9);
|
|
// A 20-wide frame is thinner than the solid square, so it refines further.
|
|
Assert.True(PolyLabel.ThicknessPrecision(Square(100), new[] { hole }) < PolyLabel.ThicknessPrecision(Square(100)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ThicknessPrecision_HoleOutsideTheOutline_FallsBackToTheOutline()
|
|
{
|
|
// A drawing of two equal rectangles side by side: the profile takes one as the
|
|
// perimeter and the other as a cutout, so the net area is zero. A near-zero precision
|
|
// refined the rectangle's whole centerline ridge and never finished.
|
|
static Polygon Rect(double x, double length, double width)
|
|
{
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(x, 0));
|
|
p.Vertices.Add(new Vector(x + length, 0));
|
|
p.Vertices.Add(new Vector(x + length, width));
|
|
p.Vertices.Add(new Vector(x, width));
|
|
return p;
|
|
}
|
|
|
|
var outer = Rect(0, 15.456693, 9.055118);
|
|
// The real drawing's second outline came out 1e-13 smaller: a net area of rounding noise.
|
|
var holes = new[] { Rect(16.401575, 15.456693 - 1e-14, 9.055118) };
|
|
|
|
var precision = PolyLabel.ThicknessPrecision(outer, holes);
|
|
Assert.Equal(PolyLabel.ThicknessPrecision(outer), precision, 12);
|
|
|
|
var pole = await Task.Run(() => PolyLabel.Find(outer, holes, precision)).WaitAsync(TimeSpan.FromSeconds(10));
|
|
Assert.True(outer.ContainsPoint(pole));
|
|
Assert.Equal(9.055118 / 2, pole.Y, 0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void DegeneratePolygon_ReturnsFallback()
|
|
{
|
|
var p = new Polygon();
|
|
p.Vertices.Add(new Vector(5, 5));
|
|
|
|
var result = PolyLabel.Find(p);
|
|
|
|
Assert.Equal(5, result.X, 0.01);
|
|
Assert.Equal(5, result.Y, 0.01);
|
|
}
|
|
}
|