mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 11:25:09 -05:00
The console overlap check and the MCP check_overlaps tool counted crossing points, so a part contained in or stacked on another reported no overlap. Both now list pairs from PartOverlapChecker.FindOverlappingPairs, which uses the Intersects verdict and skips cut-off parts. Plate.HasOverlappingParts stays for now: two test files outside this change still call it. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
198 lines
6.9 KiB
C#
198 lines
6.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest
|
|
{
|
|
/// <summary>
|
|
/// Overlap-only form of <see cref="Part.Intersects"/> for one pass over a fixed set of parts.
|
|
/// Verdicts match <c>Intersects(other, out _)</c>. Each distinct <see cref="CNC.Program"/>
|
|
/// (by reference) is prepared once, each part's world polygon is built and triangulated at
|
|
/// most once, and crossing points are not computed. Tiled copies from <see cref="Part.CloneAtOffset"/> share one
|
|
/// Program, so a fill grid prepares its pattern's programs only once.
|
|
/// Parts must not move, rotate or change Program while an instance is in use. Instances are
|
|
/// not thread-safe: create one per check.
|
|
/// </summary>
|
|
public sealed class PartOverlapChecker
|
|
{
|
|
private readonly Dictionary<CNC.Program, PreparedProgram> programs = new(
|
|
ReferenceEqualityComparer.Instance
|
|
);
|
|
|
|
private readonly Dictionary<Part, PreparedPart> preparedParts = new(
|
|
ReferenceEqualityComparer.Instance
|
|
);
|
|
|
|
/// <summary>
|
|
/// Same verdict as <c>part1.Intersects(part2, out _)</c>. Preparation stages run in the
|
|
/// same order, with the same early exits, the first time each Program is needed.
|
|
/// </summary>
|
|
public bool Overlaps(Part part1, Part part2)
|
|
{
|
|
PerfCounters.CountPartIntersects();
|
|
|
|
var prepared1 = Prepare(part1.Program);
|
|
var prepared2 = Prepare(part2.Program);
|
|
|
|
if (prepared1.Material.Count == 0 || prepared2.Material.Count == 0)
|
|
return false;
|
|
|
|
var perimeter1 = prepared1.GetPerimeter();
|
|
var perimeter2 = prepared2.GetPerimeter();
|
|
|
|
if (perimeter1 == null || perimeter2 == null)
|
|
return false;
|
|
|
|
var world1 = PreparePart(part1, prepared1);
|
|
var world2 = PreparePart(part2, prepared2);
|
|
|
|
if (world1.Polygon == null || world2.Polygon == null)
|
|
return false;
|
|
|
|
return Collision.HasOverlap(
|
|
world1.Polygon,
|
|
world1.Triangles,
|
|
world2.Polygon,
|
|
world2.Triangles
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Every overlapping pair among <paramref name="parts"/>, in list order (First precedes
|
|
/// Second). Cut-off parts are skipped. Each verdict is <see cref="Overlaps"/>, so a part
|
|
/// contained in or stacked on another is reported even though their outlines never cross.
|
|
/// </summary>
|
|
public static List<(Part First, Part Second)> FindOverlappingPairs(IEnumerable<Part> parts)
|
|
{
|
|
var pairs = new List<(Part First, Part Second)>();
|
|
var realParts = new List<Part>();
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
if (!part.BaseDrawing.IsCutOff)
|
|
realParts.Add(part);
|
|
}
|
|
|
|
var checker = new PartOverlapChecker();
|
|
|
|
for (var i = 0; i < realParts.Count; i++)
|
|
{
|
|
var part1 = realParts[i];
|
|
var b1 = part1.BoundingBox;
|
|
|
|
for (var j = i + 1; j < realParts.Count; j++)
|
|
{
|
|
var part2 = realParts[j];
|
|
var b2 = part2.BoundingBox;
|
|
|
|
// Skip pairs whose bounding boxes don't meaningfully overlap.
|
|
// Floating-point rounding can produce sub-epsilon overlaps for
|
|
// parts that are merely edge-touching, so require the overlap
|
|
// region to exceed Epsilon in both dimensions.
|
|
var overlapX =
|
|
System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
|
|
var overlapY =
|
|
System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
|
|
|
|
if (overlapX <= Math.Tolerance.Epsilon || overlapY <= Math.Tolerance.Epsilon)
|
|
continue;
|
|
|
|
if (checker.Overlaps(part1, part2))
|
|
pairs.Add((part1, part2));
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
private PreparedProgram Prepare(CNC.Program program)
|
|
{
|
|
if (!programs.TryGetValue(program, out var prepared))
|
|
{
|
|
prepared = new PreparedProgram(Part.MaterialEntities(program));
|
|
programs.Add(program, prepared);
|
|
}
|
|
|
|
return prepared;
|
|
}
|
|
|
|
private PreparedPart PreparePart(Part part, PreparedProgram prepared)
|
|
{
|
|
if (preparedParts.TryGetValue(part, out var world))
|
|
return world;
|
|
|
|
Polygon polygon = null;
|
|
var local = prepared.GetLocalPolygon();
|
|
|
|
if (local != null)
|
|
{
|
|
// Clone copies the vertices but not the bounds. Recomputing the bounds from the
|
|
// same vertices and then offsetting reproduces Part.Intersects' polygon bit for bit.
|
|
polygon = (Polygon)local.Clone();
|
|
polygon.UpdateBounds();
|
|
polygon.Offset(part.Location);
|
|
}
|
|
|
|
world = new PreparedPart(polygon);
|
|
preparedParts.Add(part, world);
|
|
return world;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A part's world polygon and, once a pair first needs it, its triangulation. Both are
|
|
/// shared by every later pair in this check and are never mutated.
|
|
/// </summary>
|
|
private sealed class PreparedPart
|
|
{
|
|
private List<Polygon> triangles;
|
|
|
|
public PreparedPart(Polygon polygon)
|
|
{
|
|
Polygon = polygon;
|
|
Triangles = GetTriangles;
|
|
}
|
|
|
|
public Polygon Polygon { get; }
|
|
|
|
public Func<List<Polygon>> Triangles { get; }
|
|
|
|
private List<Polygon> GetTriangles() => triangles ??= Collision.Triangulate(Polygon);
|
|
}
|
|
|
|
private sealed class PreparedProgram
|
|
{
|
|
private bool perimeterReady;
|
|
private Shape perimeter;
|
|
private bool polygonReady;
|
|
private Polygon localPolygon;
|
|
|
|
public PreparedProgram(List<Entity> material) => Material = material;
|
|
|
|
public List<Entity> Material { get; }
|
|
|
|
public Shape GetPerimeter()
|
|
{
|
|
if (!perimeterReady)
|
|
{
|
|
perimeter = new ShapeProfile(Material).Perimeter;
|
|
perimeterReady = true;
|
|
}
|
|
|
|
return perimeter;
|
|
}
|
|
|
|
/// <summary>Only called after <see cref="GetPerimeter"/> returned non-null.</summary>
|
|
public Polygon GetLocalPolygon()
|
|
{
|
|
if (!polygonReady)
|
|
{
|
|
localPolygon = Part.BuildOverlapPolygon(perimeter);
|
|
polygonReady = true;
|
|
}
|
|
|
|
return localPolygon;
|
|
}
|
|
}
|
|
}
|
|
}
|