mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 07:11:06 -04:00
The material-overlap diagnostic clipped every triangle of one part against every triangle of the other, then carved each fragment by every hole triangle. Curved overlaps produced thousands of fragments, and a part sitting inside another part's cutout was slow even when clear. PlateOverlapAnalyzer now intersects each candidate pair's hole-aware material once with Clipper2, in part-local coordinates on a 1e-8 grid. Each operand is its outline minus the union of its holes (positive fill rule). Regions become connected islands: a counterclockwise outline plus clockwise Holes, including an island that both parts' holes enclose together. PolygonAreaMoments.TrySubtract gives each island's net moments. A pair is reported when its total shared area exceeds Tolerance.Epsilon. Out-of-range or nonfinite coordinates make the pair incomplete rather than clear. The overlay and the cutting inspection highlight fill outlines with their holes. On 389 real PEP-converted plates the pair, clamp-finding and issue sets are identical to the previous kernel. Pair areas are equal or up to 0.0002 larger: the kernel drops fragments below its area threshold. With material prepared, the per-plate clipping maximum fell from 2.6 s to 52 ms (total 5.0 s to 0.5 s). Engine and validation checks still use Collision.
132 lines
4.9 KiB
C#
132 lines
4.9 KiB
C#
using System.Collections.Generic;
|
|
|
|
namespace OpenNest.Geometry;
|
|
|
|
/// <summary>
|
|
/// Positive area and area centroid of a simple polygon, independent of winding.
|
|
/// Moments are evaluated about a nearby origin rather than the world origin.
|
|
/// </summary>
|
|
public readonly struct PolygonAreaMoments
|
|
{
|
|
private PolygonAreaMoments(double area, Vector centroid)
|
|
{
|
|
Area = area;
|
|
Centroid = centroid;
|
|
}
|
|
|
|
public double Area { get; }
|
|
public Vector Centroid { get; }
|
|
|
|
/// <summary>
|
|
/// Accepts an open vertex list or an exactly repeated closing vertex. Returns false
|
|
/// for degenerate or nonfinite moments; does not validate polygon topology.
|
|
/// </summary>
|
|
public static bool TryCompute(IReadOnlyList<Vector> vertices, out PolygonAreaMoments moments)
|
|
{
|
|
moments = default;
|
|
if (vertices == null || vertices.Count < 3)
|
|
return false;
|
|
foreach (var point in vertices)
|
|
if (!IsFinite(point))
|
|
return false;
|
|
|
|
var origin = vertices[0];
|
|
var count = vertices.Count;
|
|
if (vertices[count - 1].X == origin.X && vertices[count - 1].Y == origin.Y)
|
|
count--;
|
|
if (count < 3)
|
|
return false;
|
|
|
|
var twiceArea = 0.0;
|
|
var momentX = 0.0;
|
|
var momentY = 0.0;
|
|
// The closing edges meet the local origin and contribute zero. The signed
|
|
// triangle fan also handles concavity without averaging polygon vertices.
|
|
for (var i = 1; i + 1 < count; i++)
|
|
{
|
|
var a = vertices[i] - origin;
|
|
var b = vertices[i + 1] - origin;
|
|
var cross = a.X * b.Y - a.Y * b.X;
|
|
twiceArea += cross;
|
|
momentX += (a.X + b.X) * cross;
|
|
momentY += (a.Y + b.Y) * cross;
|
|
}
|
|
|
|
var area = System.Math.Abs(twiceArea) * 0.5;
|
|
if (!double.IsFinite(area) || area <= 0
|
|
|| !double.IsFinite(momentX) || !double.IsFinite(momentY))
|
|
return false;
|
|
// Dividing signed moments by signed area cancels the winding, while Area
|
|
// stays positive so independently wound fragments always add material.
|
|
var centroid = origin + new Vector(momentX / twiceArea / 3, momentY / twiceArea / 3);
|
|
if (!IsFinite(centroid))
|
|
return false;
|
|
moments = new PolygonAreaMoments(area, centroid);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Combines nonoverlapping, already hole-subtracted fragments using positive area
|
|
/// weights and another local origin. The centroid may lie outside the material.
|
|
/// Empty input or any invalid fragment fails the entire result.
|
|
/// </summary>
|
|
public static bool TryCombine(IEnumerable<PolygonAreaMoments> fragments, out PolygonAreaMoments moments)
|
|
{
|
|
moments = default;
|
|
if (fragments == null)
|
|
return false;
|
|
var area = 0.0;
|
|
var origin = Vector.Zero;
|
|
var firstMoment = Vector.Zero;
|
|
foreach (var fragment in fragments)
|
|
{
|
|
if (!double.IsFinite(fragment.Area) || fragment.Area <= 0 || !IsFinite(fragment.Centroid))
|
|
return false;
|
|
if (area == 0)
|
|
origin = fragment.Centroid;
|
|
firstMoment += (fragment.Centroid - origin) * fragment.Area;
|
|
area += fragment.Area;
|
|
}
|
|
|
|
if (!double.IsFinite(area) || area <= 0 || !IsFinite(firstMoment))
|
|
return false;
|
|
var centroid = origin + firstMoment / area;
|
|
if (!IsFinite(centroid))
|
|
return false;
|
|
moments = new PolygonAreaMoments(area, centroid);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Net moments of an outline minus nonoverlapping holes that lie inside it, all given as
|
|
/// positive moments (for example from <see cref="TryCompute"/>). Moments are taken about
|
|
/// the outline's centroid. Fails for an invalid input or a nonpositive or nonfinite result.
|
|
/// </summary>
|
|
public static bool TrySubtract(PolygonAreaMoments outline, IEnumerable<PolygonAreaMoments> holes,
|
|
out PolygonAreaMoments moments)
|
|
{
|
|
moments = default;
|
|
if (holes == null || !double.IsFinite(outline.Area) || outline.Area <= 0 || !IsFinite(outline.Centroid))
|
|
return false;
|
|
var area = outline.Area;
|
|
var firstMoment = Vector.Zero;
|
|
foreach (var hole in holes)
|
|
{
|
|
if (!double.IsFinite(hole.Area) || hole.Area <= 0 || !IsFinite(hole.Centroid))
|
|
return false;
|
|
firstMoment -= (hole.Centroid - outline.Centroid) * hole.Area;
|
|
area -= hole.Area;
|
|
}
|
|
|
|
if (!double.IsFinite(area) || area <= 0 || !IsFinite(firstMoment))
|
|
return false;
|
|
var centroid = outline.Centroid + firstMoment / area;
|
|
if (!IsFinite(centroid))
|
|
return false;
|
|
moments = new PolygonAreaMoments(area, centroid);
|
|
return true;
|
|
}
|
|
|
|
private static bool IsFinite(Vector point) => double.IsFinite(point.X) && double.IsFinite(point.Y);
|
|
}
|