mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 02:41:06 -04:00
A 67-drawing, 118-part job took about six minutes through the desktop Auto Nest path and was cancelled before any layout appeared. Almost all of it was the cutout pre-pass, the rest the Irregular engine. Cutout pre-pass (317 s -> 8 s on that job, same bundles and poses): - The lattice and NFP searches prepare the frame's and the occupants' validation outlines once per search (NestLayoutCheck.Prepare) instead of re-outlining and re-triangulating the frame at every candidate, and test candidates against the small occupants before the frame. Answers match the pairwise check exactly, including its leftmost-inflated rule. - A hole's proposal is replayed for the same frame, hole and offered inserts: automatic-rotation inserts ask the same question at every frame angle. Irregular engine (40 s -> 15 s, benchmark 42 s -> 19 s, same layouts): - Concave no-fit polygons are built as the union of pairwise convex sums of each footprint's convex pieces (ear clipping plus Hertel-Mehlhorn merging on the Clipper lattice) instead of n*m edge-pair quads; a footprint that cannot be decomposed keeps the old sweep. - Pair and block catalogs share one private drawing per part geometry, owned by the engine instance, so each part's best-fit search runs once per instance and across a pipeline's trial and ordinary solves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
202 lines
7.8 KiB
C#
202 lines
7.8 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Clipper2Lib;
|
|
|
|
namespace OpenNest.Engine.NestingEngines.Irregular;
|
|
|
|
/// <summary>
|
|
/// Splits a simple polygon into convex pieces on the Clipper lattice: ear-clipping
|
|
/// triangulation followed by Hertel-Mehlhorn merging of triangles across any diagonal whose
|
|
/// removal keeps both end vertices convex. Every decision is an exact integer cross product,
|
|
/// so the pieces tile the lattice-rounded polygon exactly; a polygon the clipper cannot
|
|
/// finish (self-touching, zero-area runs) yields null and callers keep their fallback.
|
|
/// </summary>
|
|
internal static class ConvexPieces
|
|
{
|
|
/// <summary>
|
|
/// Convex CCW pieces whose union is <paramref name="polygon"/> rounded to
|
|
/// <paramref name="precision"/> decimals, or null when the polygon cannot be decomposed.
|
|
/// A convex input comes back as a single piece.
|
|
/// </summary>
|
|
public static List<PathD>? Decompose(PathD polygon, int precision)
|
|
{
|
|
var scale = System.Math.Pow(10, precision);
|
|
var points = Dedupe(Clipper.ScalePath64(polygon, scale));
|
|
if (points.Count < 3)
|
|
return null;
|
|
if (Area2(points) < 0)
|
|
points.Reverse();
|
|
if (Area2(points) <= 0)
|
|
return null;
|
|
|
|
var triangles = Triangulate(points);
|
|
if (triangles == null)
|
|
return null;
|
|
var pieces = Merge(points, triangles);
|
|
var result = new List<PathD>(pieces.Count);
|
|
foreach (var piece in pieces)
|
|
{
|
|
var path = new PathD(piece.Count);
|
|
foreach (var index in piece)
|
|
path.Add(new PointD(points[index].X / scale, points[index].Y / scale));
|
|
result.Add(path);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static List<Point64> Dedupe(Path64 source)
|
|
{
|
|
var points = new List<Point64>(source.Count);
|
|
foreach (var point in source)
|
|
if (points.Count == 0 || points[^1] != point)
|
|
points.Add(point);
|
|
while (points.Count > 1 && points[0] == points[^1])
|
|
points.RemoveAt(points.Count - 1);
|
|
return points;
|
|
}
|
|
|
|
/// <summary>Twice the signed area; positive for CCW. Measured from a vertex so that far-off
|
|
/// outlines do not overflow.</summary>
|
|
private static Int128 Area2(List<Point64> points)
|
|
{
|
|
Int128 area = 0;
|
|
for (var i = 1; i + 1 < points.Count; i++)
|
|
area += Cross(points[0], points[i], points[i + 1]);
|
|
return area;
|
|
}
|
|
|
|
private static Int128 Cross(Point64 a, Point64 b, Point64 c) =>
|
|
((Int128)b.X - a.X) * ((Int128)c.Y - a.Y) - ((Int128)b.Y - a.Y) * ((Int128)c.X - a.X);
|
|
|
|
/// <summary>
|
|
/// Ear clipping over vertex indices. An ear is a strictly convex corner whose triangle has
|
|
/// no other vertex inside or on its boundary; zero-area corners are never clipped, so a
|
|
/// polygon that degenerates to collinear remains yields null.
|
|
/// </summary>
|
|
private static List<int[]>? Triangulate(List<Point64> points)
|
|
{
|
|
var ring = new List<int>(points.Count);
|
|
for (var i = 0; i < points.Count; i++)
|
|
ring.Add(i);
|
|
var triangles = new List<int[]>(points.Count);
|
|
var start = 0;
|
|
while (ring.Count > 3)
|
|
{
|
|
var found = false;
|
|
for (var attempt = 0; attempt < ring.Count; attempt++)
|
|
{
|
|
var at = (start + attempt) % ring.Count;
|
|
var previous = ring[(at + ring.Count - 1) % ring.Count];
|
|
var current = ring[at];
|
|
var next = ring[(at + 1) % ring.Count];
|
|
if (!IsEar(points, ring, previous, current, next))
|
|
continue;
|
|
triangles.Add(new[] { previous, current, next });
|
|
ring.RemoveAt(at);
|
|
start = at % ring.Count;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (!found)
|
|
return null;
|
|
}
|
|
if (Cross(points[ring[0]], points[ring[1]], points[ring[2]]) <= 0)
|
|
return null;
|
|
triangles.Add(new[] { ring[0], ring[1], ring[2] });
|
|
return triangles;
|
|
}
|
|
|
|
private static bool IsEar(List<Point64> points, List<int> ring, int previous, int current, int next)
|
|
{
|
|
var a = points[previous];
|
|
var b = points[current];
|
|
var c = points[next];
|
|
if (Cross(a, b, c) <= 0)
|
|
return false;
|
|
foreach (var index in ring)
|
|
{
|
|
if (index == previous || index == current || index == next)
|
|
continue;
|
|
var p = points[index];
|
|
if (p == a || p == b || p == c)
|
|
continue;
|
|
if (Cross(a, b, p) >= 0 && Cross(b, c, p) >= 0 && Cross(c, a, p) >= 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hertel-Mehlhorn: drop every diagonal whose two end corners stay convex (or straight)
|
|
/// without it. Pieces are cyclic index lists; a diagonal is an edge whose reverse lies in
|
|
/// another piece.
|
|
/// </summary>
|
|
private static List<List<int>> Merge(List<Point64> points, List<int[]> triangles)
|
|
{
|
|
var pieces = new List<List<int>?>(triangles.Count);
|
|
foreach (var triangle in triangles)
|
|
pieces.Add(new List<int>(triangle));
|
|
// Directed edge (from, to) -> piece holding it.
|
|
var owner = new Dictionary<(int, int), int>();
|
|
for (var p = 0; p < pieces.Count; p++)
|
|
foreach (var edge in Edges(pieces[p]!))
|
|
owner[edge] = p;
|
|
|
|
for (var p = 0; p < pieces.Count; p++)
|
|
{
|
|
var merged = true;
|
|
while (merged && pieces[p] != null)
|
|
{
|
|
merged = false;
|
|
var piece = pieces[p]!;
|
|
for (var e = 0; e < piece.Count; e++)
|
|
{
|
|
var i = piece[e];
|
|
var j = piece[(e + 1) % piece.Count];
|
|
if (!owner.TryGetValue((j, i), out var q) || q == p || pieces[q] == null)
|
|
continue;
|
|
var other = pieces[q]!;
|
|
// piece: ... a, i, j, b ... other: ... c, j, i, d ...
|
|
var a = piece[(e + piece.Count - 1) % piece.Count];
|
|
var b = piece[(e + 2) % piece.Count];
|
|
var f = other.IndexOf(j);
|
|
var c = other[(f + other.Count - 1) % other.Count];
|
|
var d = other[(f + 2) % other.Count];
|
|
if (Cross(points[a], points[i], points[d]) < 0 || Cross(points[c], points[j], points[b]) < 0)
|
|
continue;
|
|
|
|
// Walk piece from j around to i, then other from i around to j.
|
|
var union = new List<int>(piece.Count + other.Count - 2);
|
|
for (var k = 0; k < piece.Count - 1; k++)
|
|
union.Add(piece[(e + 1 + k) % piece.Count]);
|
|
for (var k = 0; k < other.Count - 1; k++)
|
|
union.Add(other[(f + 1 + k) % other.Count]);
|
|
foreach (var edge in Edges(other))
|
|
owner.Remove(edge);
|
|
foreach (var edge in Edges(piece))
|
|
owner.Remove(edge);
|
|
foreach (var edge in Edges(union))
|
|
owner[edge] = p;
|
|
pieces[p] = union;
|
|
pieces[q] = null;
|
|
merged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var result = new List<List<int>>();
|
|
foreach (var piece in pieces)
|
|
if (piece != null)
|
|
result.Add(piece);
|
|
return result;
|
|
}
|
|
|
|
private static IEnumerable<(int, int)> Edges(List<int> piece)
|
|
{
|
|
for (var k = 0; k < piece.Count; k++)
|
|
yield return (piece[k], piece[(k + 1) % piece.Count]);
|
|
}
|
|
}
|