fix(ui): scale part label search precision to material width

The plate view searched for each part's label point with PolyLabel's fixed
0.5" precision, a third of a 1.5" part, so the search stopped on the first
interior cell and put labels near edges instead of in the thickest spot.
Precision is now 5% of the part's mean material width (2 x net area over
total outline length), so small, slender or heavily cut-out parts refine
further while large solid ones stay cheap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-19 11:50:41 -04:00
1 parent add94dff6c
commit 3d1270a860
3 files changed
+76 -1

No files matched your search

+37
View File
@@ -24,6 +24,43 @@ namespace OpenNest.Geometry
public static Vector Find(Polygon outer, IList<Polygon> holes, double precision, int maxCells) =>
Find(outer, holes, precision, maxCells, out _);
/// <summary>
/// A precision proportional to the shape's mean material width (twice the net area over
/// the total outline length, holes included), so small or slender shapes refine further
/// than large solid ones. A fixed precision near the shape's size stops at the first
/// interior cell.
/// </summary>
public static double ThicknessPrecision(Polygon outer, IList<Polygon> holes = null, double fraction = 0.05)
{
var area = outer.Area();
var length = ClosedLength(outer);
if (holes != null)
{
for (var i = 0; i < holes.Count; i++)
{
area -= holes[i].Area();
length += ClosedLength(holes[i]);
}
}
if (!(area > 0) || !(length > 0))
return 1e-9;
return System.Math.Max(fraction * 2 * area / length, 1e-9);
}
private static double ClosedLength(Polygon polygon)
{
var verts = polygon.Vertices;
var sum = 0.0;
for (int i = 0, j = verts.Count - 1; i < verts.Count; j = i++)
sum += verts[j].DistanceTo(verts[i]);
return sum;
}
internal static Vector Find(
Polygon outer,
IList<Polygon> holes,
+38
View File
@@ -115,6 +115,44 @@ public class PolyLabelTests
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 centreline 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 void DegeneratePolygon_ReturnsFallback()
{
+1 -1
View File
@@ -237,7 +237,7 @@ namespace OpenNest
holes.Add(cutout.ToPolygonWithTolerance(0.1));
}
return PolyLabel.Find(outer, holes);
return PolyLabel.Find(outer, holes, PolyLabel.ThicknessPrecision(outer, holes));
}
public void Update(DrawControl plateView)