From 3d1270a8603b2dadcce0ade69a865b99c8bc78ce Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 19 Oct 2026 11:50:41 -0400 Subject: [PATCH] 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 --- OpenNest.Core/Geometry/PolyLabel.cs | 37 ++++++++++++++++++++++ OpenNest.Tests/Geometry/PolyLabelTests.cs | 38 +++++++++++++++++++++++ OpenNest/LayoutPart.cs | 2 +- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/OpenNest.Core/Geometry/PolyLabel.cs b/OpenNest.Core/Geometry/PolyLabel.cs index b7362044..7bcef39b 100644 --- a/OpenNest.Core/Geometry/PolyLabel.cs +++ b/OpenNest.Core/Geometry/PolyLabel.cs @@ -24,6 +24,43 @@ namespace OpenNest.Geometry public static Vector Find(Polygon outer, IList holes, double precision, int maxCells) => Find(outer, holes, precision, maxCells, out _); + /// + /// 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. + /// + public static double ThicknessPrecision(Polygon outer, IList 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 holes, diff --git a/OpenNest.Tests/Geometry/PolyLabelTests.cs b/OpenNest.Tests/Geometry/PolyLabelTests.cs index 1d333ba1..55c3ea32 100644 --- a/OpenNest.Tests/Geometry/PolyLabelTests.cs +++ b/OpenNest.Tests/Geometry/PolyLabelTests.cs @@ -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() { diff --git a/OpenNest/LayoutPart.cs b/OpenNest/LayoutPart.cs index d7b6c887..af759887 100644 --- a/OpenNest/LayoutPart.cs +++ b/OpenNest/LayoutPart.cs @@ -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)