mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 06:51:09 -05:00
fix(core): report overlapping pairs through PartOverlapChecker
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>
This commit is contained in:
1 parent
a451cf5ef3
commit
e5d4337681
11 files changed
+179
-38
No files matched your search
@@ -789,14 +789,14 @@ static class NestConsole
|
||||
var total = 0;
|
||||
foreach (var plate in plates.Where(p => p.Parts.Count > 0))
|
||||
{
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
||||
var overlapping = PartOverlapChecker.FindOverlappingPairs(plate.Parts);
|
||||
var label = plates.Count > 1 ? $"Plate {nest.Plates.IndexOf(plate)}: " : "";
|
||||
Console.WriteLine(
|
||||
hasOverlaps
|
||||
? $"{label}OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
||||
overlapping.Count > 0
|
||||
? $"{label}OVERLAPS DETECTED: {overlapping.Count} overlapping pair(s)"
|
||||
: $"{label}Overlap check: PASS"
|
||||
);
|
||||
total += overlapPts.Count;
|
||||
total += overlapping.Count;
|
||||
}
|
||||
|
||||
return total;
|
||||
|
||||
@@ -57,6 +57,54 @@ namespace OpenNest
|
||||
);
|
||||
}
|
||||
|
||||
/// <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))
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace OpenNest.Mcp.Tools
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "check_overlaps")]
|
||||
[Description("Check a plate for overlapping parts. Reports collision points if any.")]
|
||||
[Description("Check a plate for overlapping parts. Reports the overlapping pairs by part index if any.")]
|
||||
public string CheckOverlaps([Description("Index of the plate")] int plateIndex)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
@@ -119,24 +119,26 @@ namespace OpenNest.Mcp.Tools
|
||||
if (plate.Parts.Count < 2)
|
||||
return $"Plate {plateIndex}: no overlaps possible (fewer than 2 parts)";
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var pts);
|
||||
var pairs = PartOverlapChecker.FindOverlappingPairs(plate.Parts);
|
||||
|
||||
if (!hasOverlaps)
|
||||
if (pairs.Count == 0)
|
||||
return $"Plate {plateIndex}: no overlapping parts detected";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Plate {plateIndex}: {pts.Count} collision point(s) detected!");
|
||||
sb.AppendLine($"Plate {plateIndex}: {pairs.Count} overlapping pair(s) detected!");
|
||||
|
||||
var limit = System.Math.Min(pts.Count, 20);
|
||||
var limit = System.Math.Min(pairs.Count, 20);
|
||||
|
||||
for (var i = 0; i < limit; i++)
|
||||
{
|
||||
var pt = pts[i];
|
||||
sb.AppendLine($" Collision at ({pt.X:F2}, {pt.Y:F2})");
|
||||
var (first, second) = pairs[i];
|
||||
sb.AppendLine(
|
||||
$" Part {plate.Parts.IndexOf(first)} overlaps part {plate.Parts.IndexOf(second)}"
|
||||
);
|
||||
}
|
||||
|
||||
if (pts.Count > limit)
|
||||
sb.AppendLine($" ... and {pts.Count - limit} more collision points");
|
||||
if (pairs.Count > limit)
|
||||
sb.AppendLine($" ... and {pairs.Count - limit} more overlapping pairs");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ public class CommonLinePlanningTests
|
||||
nest.Drawings.Add(drawing);
|
||||
var utilization = plate.Utilization();
|
||||
var nested = drawing.Quantity.Nested;
|
||||
var overlaps = plate.HasOverlappingParts(out _);
|
||||
var overlaps = PartOverlapChecker.FindOverlappingPairs(plate.Parts).Count;
|
||||
var pairs = PlateOverlapAnalyzer.Analyze(plate.Parts.ToList()).Pairs.Count;
|
||||
var area = plate.Parts.Sum(p => p.BaseDrawing.Area);
|
||||
|
||||
@@ -267,7 +267,7 @@ public class CommonLinePlanningTests
|
||||
Assert.All(plate.Parts.Skip(1), p => Assert.True(CommonLineCuts.IsSinglePass(p.Program)));
|
||||
Assert.Equal(utilization, plate.Utilization(), 12);
|
||||
Assert.Equal(nested, drawing.Quantity.Nested);
|
||||
Assert.Equal(overlaps, plate.HasOverlappingParts(out _));
|
||||
Assert.Equal(overlaps, PartOverlapChecker.FindOverlappingPairs(plate.Parts).Count);
|
||||
Assert.Equal(pairs, PlateOverlapAnalyzer.Analyze(plate.Parts.ToList()).Pairs.Count);
|
||||
Assert.Equal(area, plate.Parts.Sum(p => p.BaseDrawing.Area), 12);
|
||||
Assert.True(CommonLineGeometry.Check(plate.CommonLines[0]).IsIntact);
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CutOffTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_HasOverlappingParts_SkipsCutOffParts()
|
||||
public void FindOverlappingPairs_SkipsCutOffParts()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
@@ -60,8 +60,30 @@ public class CutOffTests
|
||||
plate.Parts.Add(new Part(realDrawing));
|
||||
plate.Parts.Add(new Part(cutoffDrawing));
|
||||
|
||||
var hasOverlap = plate.HasOverlappingParts(out var pts);
|
||||
Assert.False(hasOverlap);
|
||||
Assert.Empty(PartOverlapChecker.FindOverlappingPairs(plate.Parts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOverlappingPairs_ReportsTheSameStackedPartsWhenNeitherIsACutOff()
|
||||
{
|
||||
// Control for the test above: the same two stacked squares overlap, so only the
|
||||
// cut-off flag keeps that pair out of the result.
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
|
||||
var plate = new Plate(100, 100);
|
||||
var first = new Part(new Drawing("real", pgm));
|
||||
var second = new Part(new Drawing("also real", pgm));
|
||||
plate.Parts.Add(first);
|
||||
plate.Parts.Add(second);
|
||||
|
||||
var pair = Assert.Single(PartOverlapChecker.FindOverlappingPairs(plate.Parts));
|
||||
Assert.Same(first, pair.First);
|
||||
Assert.Same(second, pair.Second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -62,18 +62,15 @@ public class EngineOverlapTests
|
||||
foreach (var g in rotGroups)
|
||||
_output.WriteLine($" Rotation {g.Key:F1}°: {g.Count()} parts");
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var collisionPoints);
|
||||
_output.WriteLine($"Overlaps: {hasOverlaps} ({collisionPoints.Count} collision pts)");
|
||||
var pairs = PartOverlapChecker.FindOverlappingPairs(plate.Parts);
|
||||
_output.WriteLine($"Overlaps: {pairs.Count > 0} ({pairs.Count} overlapping pair(s))");
|
||||
|
||||
if (hasOverlaps)
|
||||
{
|
||||
for (var i = 0; i < System.Math.Min(collisionPoints.Count, 10); i++)
|
||||
_output.WriteLine($" ({collisionPoints[i].X:F2}, {collisionPoints[i].Y:F2})");
|
||||
}
|
||||
foreach (var (first, second) in pairs.Take(10))
|
||||
_output.WriteLine($" part {plate.Parts.IndexOf(first)} overlaps part {plate.Parts.IndexOf(second)}");
|
||||
|
||||
Assert.False(
|
||||
hasOverlaps,
|
||||
$"Strategy '{strategy}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"
|
||||
Assert.True(
|
||||
pairs.Count == 0,
|
||||
$"Strategy '{strategy}' produced {pairs.Count} overlapping pair(s) with {plate.Parts.Count} parts"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,11 +84,11 @@ public class EngineOverlapTests
|
||||
TestHelpers.MakePartAt(10, 0, 10)
|
||||
);
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var pts);
|
||||
_output.WriteLine($"Adjacent squares: overlaps={hasOverlaps}, collision count={pts.Count}");
|
||||
var pairs = PartOverlapChecker.FindOverlappingPairs(plate.Parts);
|
||||
_output.WriteLine($"Adjacent squares: overlapping pairs={pairs.Count}");
|
||||
|
||||
Assert.False(
|
||||
hasOverlaps,
|
||||
Assert.True(
|
||||
pairs.Count == 0,
|
||||
"Adjacent edge-touching parts should not be reported as overlapping"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class EngineRefactorSmokeTests
|
||||
|
||||
Assert.Equal(23, parts.Count);
|
||||
plate.Parts.AddRange(parts);
|
||||
Assert.False(plate.HasOverlappingParts(out _), "Fill overlaps");
|
||||
Assert.Empty(PartOverlapChecker.FindOverlappingPairs(plate.Parts));
|
||||
var area = plate.WorkArea();
|
||||
Assert.All(parts, p => Assert.True(
|
||||
p.BoundingBox.Left >= area.Left - 1e-6 && p.BoundingBox.Right <= area.Right + 1e-6
|
||||
|
||||
@@ -79,7 +79,7 @@ public class FillLinearMaxPartsTests
|
||||
Assert.Equal(3, parts.Count);
|
||||
var plate = new Plate(width, length) { PartSpacing = Spacing };
|
||||
plate.Parts.AddRange(parts);
|
||||
Assert.False(plate.HasOverlappingParts(out _));
|
||||
Assert.Empty(PartOverlapChecker.FindOverlappingPairs(plate.Parts));
|
||||
Assert.All(parts, part => Assert.True(part.BoundingBox.Right <= area.Right
|
||||
&& part.BoundingBox.Top <= area.Top));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class PairRemnantFillerTests
|
||||
|
||||
var check = new Plate { Size = new Size(60, 120), PartSpacing = Spacing };
|
||||
check.Parts.AddRange(withPairs);
|
||||
Assert.False(check.HasOverlappingParts(out _));
|
||||
Assert.Empty(PartOverlapChecker.FindOverlappingPairs(check.Parts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -411,6 +411,76 @@ public class PartOverlapCheckerTests
|
||||
return pattern;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOverlappingPairs_ReportsAContainedPart()
|
||||
{
|
||||
// The small square sits wholly inside the large one: no outlines cross, so there
|
||||
// are no crossing points to count, but the material overlaps.
|
||||
var outer = TestHelpers.MakePartAt(0, 0, 10);
|
||||
var inner = TestHelpers.MakePartAt(4, 4, 2);
|
||||
|
||||
Assert.True(outer.Intersects(inner, out var points));
|
||||
Assert.Empty(points);
|
||||
|
||||
var pair = Assert.Single(PartOverlapChecker.FindOverlappingPairs(new[] { outer, inner }));
|
||||
Assert.Same(outer, pair.First);
|
||||
Assert.Same(inner, pair.Second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOverlappingPairs_IgnoresEdgeTouchingParts()
|
||||
{
|
||||
var parts = new[]
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(10, 0, 10),
|
||||
TestHelpers.MakePartAt(0, 10, 10),
|
||||
TestHelpers.MakePartAt(10, 10, 10),
|
||||
};
|
||||
|
||||
Assert.Empty(PartOverlapChecker.FindOverlappingPairs(parts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOverlappingPairs_ExcludesCutOffParts()
|
||||
{
|
||||
var real = TestHelpers.MakePartAt(0, 0, 10);
|
||||
var other = TestHelpers.MakePartAt(5, 5, 10);
|
||||
var cutOff = TestHelpers.MakePartAt(2, 2, 10);
|
||||
cutOff.BaseDrawing.IsCutOff = true;
|
||||
|
||||
var pair = Assert.Single(PartOverlapChecker.FindOverlappingPairs(new[] { real, cutOff, other }));
|
||||
Assert.Same(real, pair.First);
|
||||
Assert.Same(other, pair.Second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOverlappingPairs_MatchesPartIntersectsOnASmallGrid()
|
||||
{
|
||||
// 10-wide squares on a 7 pitch: neighbors (diagonal ones too) overlap, squares two
|
||||
// steps apart are clear, and one small square is contained in the middle one.
|
||||
var parts = new List<Part>();
|
||||
for (var row = 0; row < 3; row++)
|
||||
for (var column = 0; column < 3; column++)
|
||||
parts.Add(TestHelpers.MakePartAt(column * 7, row * 7, 10));
|
||||
parts.Add(TestHelpers.MakePartAt(11, 11, 2));
|
||||
|
||||
var expected = new List<(int, int)>();
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
if (parts[i].Intersects(parts[j], out _))
|
||||
expected.Add((i, j));
|
||||
|
||||
var actual = PartOverlapChecker.FindOverlappingPairs(parts)
|
||||
.Select(pair => (parts.IndexOf(pair.First), parts.IndexOf(pair.Second)))
|
||||
.ToList();
|
||||
|
||||
Assert.Contains((0, 1), expected);
|
||||
Assert.DoesNotContain((0, 2), expected);
|
||||
Assert.Contains((4, 9), expected);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
|
||||
|
||||
private static long[] PolygonBits(Polygon polygon) => new[]
|
||||
|
||||
@@ -91,9 +91,11 @@ material), even if the dialog is later canceled. There is no check during export
|
||||
|
||||
`OpenNest.Diagnostics.PlateOverlapAnalyzer` in OpenNest.Core checks a group of placed
|
||||
parts and returns the shared material for each overlapping pair as connected
|
||||
islands. It clips with Clipper2. Existing `Part.Intersects`, `PartOverlapChecker`,
|
||||
`Plate.HasOverlappingParts`, engine validators, and CLI entry points are unchanged
|
||||
and still use the hand-written `Collision` kernel (its GPU port is on hold).
|
||||
islands. It clips with Clipper2. Existing `Part.Intersects`, `PartOverlapChecker` and
|
||||
engine validators are unchanged and still use the hand-written `Collision` kernel (its
|
||||
GPU port is on hold). The console's overlap check and the MCP `check_overlaps` tool list
|
||||
overlapping pairs through `PartOverlapChecker.FindOverlappingPairs`, which skips cut-off
|
||||
parts and reports a contained or stacked part even though no outlines cross.
|
||||
|
||||
## Synchronous use
|
||||
|
||||
|
||||
Reference in new issue
Block a user