feat: add polylabel algorithm for part label positioning and README

Use pole-of-inaccessibility (polylabel) to place part labels at the
visual center of shapes instead of the first path vertex. Labels now
stay correctly positioned regardless of part rotation or shape.

Also adds project README and MIT license.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 08:52:48 -04:00
parent a0865405e2
commit 224fbde19a
4 changed files with 363 additions and 3 deletions

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
/// <summary>
/// Finds the pole of inaccessibility — the point inside a polygon that is
/// farthest from any edge. Based on the polylabel algorithm by Mapbox.
/// </summary>
public static class PolyLabel
{
public static Vector Find(List<Vector> vertices, double precision = 1.0)
{
if (vertices == null || vertices.Count < 3)
return Vector.Zero;
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
for (var i = 0; i < vertices.Count; i++)
{
var v = vertices[i];
if (v.X < minX) minX = v.X;
if (v.Y < minY) minY = v.Y;
if (v.X > maxX) maxX = v.X;
if (v.Y > maxY) maxY = v.Y;
}
var width = maxX - minX;
var height = maxY - minY;
var cellSize = System.Math.Min(width, height);
if (cellSize == 0)
return new Vector(minX, minY);
var halfCell = cellSize / 2.0;
// Priority queue (sorted list, largest distance first)
var queue = new List<Cell>();
for (var x = minX; x < maxX; x += cellSize)
{
for (var y = minY; y < maxY; y += cellSize)
{
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, vertices));
}
}
queue.Sort((a, b) => b.Max.CompareTo(a.Max));
var bestCell = GetCentroidCell(vertices);
var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, vertices);
if (bboxCell.Distance > bestCell.Distance)
bestCell = bboxCell;
while (queue.Count > 0)
{
var cell = queue[queue.Count - 1];
queue.RemoveAt(queue.Count - 1);
if (cell.Distance > bestCell.Distance)
bestCell = cell;
if (cell.Max - bestCell.Distance <= precision)
continue;
halfCell = cell.HalfSize / 2;
var c1 = new Cell(cell.X - halfCell, cell.Y - halfCell, halfCell, vertices);
var c2 = new Cell(cell.X + halfCell, cell.Y - halfCell, halfCell, vertices);
var c3 = new Cell(cell.X - halfCell, cell.Y + halfCell, halfCell, vertices);
var c4 = new Cell(cell.X + halfCell, cell.Y + halfCell, halfCell, vertices);
InsertSorted(queue, c1);
InsertSorted(queue, c2);
InsertSorted(queue, c3);
InsertSorted(queue, c4);
}
return new Vector(bestCell.X, bestCell.Y);
}
private static void InsertSorted(List<Cell> queue, Cell cell)
{
var index = queue.BinarySearch(cell, CellComparer.Instance);
if (index < 0) index = ~index;
queue.Insert(index, cell);
}
private static Cell GetCentroidCell(List<Vector> vertices)
{
var area = 0.0;
var cx = 0.0;
var cy = 0.0;
var n = vertices.Count;
for (int i = 0, j = n - 1; i < n; j = i++)
{
var a = vertices[i];
var b = vertices[j];
var f = a.X * b.Y - b.X * a.Y;
cx += (a.X + b.X) * f;
cy += (a.Y + b.Y) * f;
area += f * 3;
}
if (area == 0)
return new Cell(vertices[0].X, vertices[0].Y, 0, vertices);
return new Cell(cx / area, cy / area, 0, vertices);
}
private static double PointToPolygonDistance(double x, double y, List<Vector> vertices)
{
var inside = false;
var minDistSq = double.MaxValue;
var n = vertices.Count;
for (int i = 0, j = n - 1; i < n; j = i++)
{
var a = vertices[i];
var b = vertices[j];
if ((a.Y > y) != (b.Y > y) &&
x < (b.X - a.X) * (y - a.Y) / (b.Y - a.Y) + a.X)
{
inside = !inside;
}
var distSq = SegmentDistanceSq(x, y, a.X, a.Y, b.X, b.Y);
if (distSq < minDistSq)
minDistSq = distSq;
}
var dist = System.Math.Sqrt(minDistSq);
return inside ? dist : -dist;
}
private static double SegmentDistanceSq(double px, double py,
double ax, double ay, double bx, double by)
{
var dx = bx - ax;
var dy = by - ay;
if (dx != 0 || dy != 0)
{
var t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
if (t > 1)
{
ax = bx;
ay = by;
}
else if (t > 0)
{
ax += dx * t;
ay += dy * t;
}
}
dx = px - ax;
dy = py - ay;
return dx * dx + dy * dy;
}
private struct Cell
{
public readonly double X;
public readonly double Y;
public readonly double HalfSize;
public readonly double Distance;
public readonly double Max;
public Cell(double x, double y, double halfSize, List<Vector> vertices)
{
X = x;
Y = y;
HalfSize = halfSize;
Distance = PointToPolygonDistance(x, y, vertices);
Max = Distance + halfSize * System.Math.Sqrt(2);
}
}
private class CellComparer : IComparer<Cell>
{
public static readonly CellComparer Instance = new CellComparer();
public int Compare(Cell a, Cell b) => b.Max.CompareTo(a.Max);
}
}
}