mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-20 19:41:40 -04:00
Collision.Check finds no overlap region for a clamp smaller than its tolerance, so Check Overlaps reported a part covering a 1e-5 clamp as clear. Clamps below 0.001 are judged with KeepOutOverlap and reported as a finding whose fragment is the whole clamp rectangle; flush and clear parts stay clear. PM aa995123-295f-479c-adc4-b0cbcb7cf838
222 lines
9.2 KiB
C#
222 lines
9.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Diagnostics;
|
|
using OpenNest.Geometry;
|
|
using Xunit;
|
|
|
|
namespace OpenNest.Tests.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// Slice 4 (C1): the material-overlap analyzer reports part-vs-clamp findings like
|
|
/// other overlap findings - owned fragments, area and centroid - and they flow into
|
|
/// hover text and paging. Files/plates without zones behave exactly as before.
|
|
/// </summary>
|
|
public class PlateOverlapAnalyzerClampZoneTests
|
|
{
|
|
private static Program Rect(double lengthX, double widthY)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(0, 0);
|
|
program.LineTo(lengthX, 0);
|
|
program.LineTo(lengthX, widthY);
|
|
program.LineTo(0, widthY);
|
|
program.LineTo(0, 0);
|
|
return program;
|
|
}
|
|
|
|
private static Drawing Square(string name, double size) => new(name, Rect(size, size));
|
|
|
|
private static readonly ClampZone Front1 = new("front-1", new Box(10, 0, 1.5, 0.625));
|
|
private static readonly ClampZone Front2 = new("front-2", new Box(20, 0, 1.5, 0.625));
|
|
|
|
[Fact]
|
|
public void ManualCutoffCrossingClampAppearsInOverlapCheckWithoutBeingMaterial()
|
|
{
|
|
var plate = new Plate(new Size(24, 48));
|
|
plate.ClampZones.Add(Front1);
|
|
plate.CutOffs.Add(new CutOff(new Vector(10.5, 0), CutOffAxis.Vertical));
|
|
var report = PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.CapturePlate(plate));
|
|
Assert.Empty(report.Pairs);
|
|
Assert.Empty(report.ClampFindings);
|
|
Assert.True(report.IsComplete);
|
|
var finding = Assert.Single(report.CutOffClampFindings);
|
|
Assert.Equal("front-1", finding.ClampId);
|
|
Assert.Equal(0, finding.CutOffIndex);
|
|
Assert.InRange(finding.Location.X, 10, 11.5);
|
|
var state = new OverlapReportState();
|
|
Assert.True(state.TryPublish(state.Begin(plate), plate, report));
|
|
Assert.Contains("cut-off", state.Message);
|
|
plate.CutOffs.Clear();
|
|
Assert.False(state.EnsureFresh(plate));
|
|
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void PartOverlappingClampProducesHoleAwareFinding()
|
|
{
|
|
// 4x4 square at (9.25, 0): the shared fragment with front-1 is exactly its footprint.
|
|
var parts = new List<Part> { new(Square("plate-part", 4), new Vector(9.25, 0)) };
|
|
|
|
var report = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { Front1, Front2 }));
|
|
|
|
Assert.True(report.IsComplete, string.Join("; ", report.Issues.Select(issue => issue.Message)));
|
|
var finding = Assert.Single(report.ClampFindings);
|
|
Assert.Equal(0, finding.PartAId);
|
|
Assert.Equal("plate-part", finding.PartAName);
|
|
Assert.Contains("front-1", finding.PartBName);
|
|
Assert.Equal(1.5 * 0.625, finding.Area, 3);
|
|
Assert.InRange(finding.Centroid.X, 10 - 1e-3, 11.5 + 1e-3);
|
|
Assert.InRange(finding.Centroid.Y, 0 - 1e-3, 0.625 + 1e-3);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClearPartsProduceNoClampFindings()
|
|
{
|
|
var parts = new List<Part>
|
|
{
|
|
new(Square("far", 4), new Vector(30, 10)),
|
|
new(Square("behind", 2), new Vector(10, 5)), // between the clamps, above them
|
|
};
|
|
|
|
var report = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { Front1, Front2 }));
|
|
|
|
Assert.True(report.IsComplete);
|
|
Assert.Empty(report.Pairs);
|
|
Assert.Empty(report.ClampFindings);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyCaptureFindsNoClampsAndKeepsPartPairs()
|
|
{
|
|
var parts = new List<Part>
|
|
{
|
|
new(Square("a", 4), new Vector(0, 0)),
|
|
new(Square("b", 4), new Vector(3, 0)), // overlaps a
|
|
};
|
|
|
|
var legacy = PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.Capture(parts));
|
|
Assert.Single(legacy.Pairs);
|
|
Assert.Empty(legacy.ClampFindings);
|
|
|
|
var explicitEmpty = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, System.Array.Empty<ClampZone>()));
|
|
Assert.Equal(Describe(legacy), Describe(explicitEmpty));
|
|
}
|
|
|
|
[Fact]
|
|
public void IncrementalRecheckAfterMovingAClampMatchesAFullAnalysis()
|
|
{
|
|
var parts = new List<Part>
|
|
{
|
|
new(Square("a", 4), new Vector(9.25, 0)), // hits the zone at X 10
|
|
new(Square("b", 4), new Vector(30, 10)), // clear of the zone at X 10
|
|
};
|
|
var moved = new ClampZone("front-1", new Box(29, 10, 1.5, 0.625)); // now hits b
|
|
|
|
var previous = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { Front1 }));
|
|
Assert.Equal(0, Assert.Single(previous.ClampFindings).PartAId);
|
|
|
|
var incremental = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { moved }), previous);
|
|
var oracle = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { moved }));
|
|
|
|
Assert.Equal(Describe(oracle), Describe(incremental));
|
|
Assert.Equal(1, Assert.Single(incremental.ClampFindings).PartAId);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindingsRenderAsClampDetailsAndPageLikeOtherFindings()
|
|
{
|
|
var parts = new List<Part>
|
|
{
|
|
new(Square("hit-one", 4), new Vector(9.25, 0)),
|
|
new(Square("hit-two", 4), new Vector(19.25, 0)),
|
|
new(Square("also-hit-two", 4), new Vector(20.5, 0.3)),
|
|
};
|
|
|
|
var report = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(parts, new[] { Front1, Front2 }));
|
|
|
|
Assert.Equal(3, report.ClampFindings.Count); // hit-one/1, hit-two/2, hit-two/3 -> two zones each
|
|
var finding = report.ClampFindings.First(f => f.PartAName == "hit-one");
|
|
Assert.True(OverlapPairPresentation.IsClampFinding(finding));
|
|
var details = OverlapPairPresentation.Details(finding, Units.Inches, CultureInfo.InvariantCulture);
|
|
Assert.Contains("front-1", details);
|
|
Assert.Contains("hit-one", details);
|
|
Assert.DoesNotContain("Pair ", details);
|
|
|
|
// Hover text joins every finding; paging must reach all of them.
|
|
var text = string.Join("\n\n", report.ClampFindings.Select(f =>
|
|
OverlapPairPresentation.Details(f, Units.Inches, CultureInfo.InvariantCulture)));
|
|
var pages = OverlapHoverPages.Create(text, report.ClampFindings.Count, 6, 60, s => s.Length);
|
|
Assert.True(pages.PageCount > 1);
|
|
var all = string.Concat(Enumerable.Range(0, pages.PageCount)
|
|
.Select(page => { pages.MovePage(page == 0 ? 0 : 1); return string.Concat(pages.Lines); }));
|
|
Assert.Contains("front-2", all);
|
|
Assert.Contains("also-hit-two", all);
|
|
}
|
|
|
|
[Fact]
|
|
public void PublishedClampFindingIsVisibleAndZoneEditInvalidatesIt()
|
|
{
|
|
var plate = new Plate(new Size(24, 48));
|
|
plate.Parts.Add(new Part(Square("hit", 4), new Vector(9.25, 0)));
|
|
plate.ClampZones.Add(Front1);
|
|
var state = new OverlapReportState();
|
|
var generation = state.Begin(plate);
|
|
var report = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), plate.ClampZones.ToArray()));
|
|
Assert.True(state.TryPublish(generation, plate, report));
|
|
Assert.Single(state.DisplayPairs);
|
|
Assert.Contains("clamp", state.Message, System.StringComparison.OrdinalIgnoreCase);
|
|
plate.ClampZones.Clear();
|
|
state.Invalidate(plate);
|
|
Assert.Empty(state.DisplayPairs);
|
|
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void PublicationRefusesZonesChangedWhileWorkerWasRunning()
|
|
{
|
|
var plate = new Plate(new Size(24, 48));
|
|
plate.Parts.Add(new Part(Square("hit", 4), new Vector(9.25, 0)));
|
|
plate.ClampZones.Add(Front1);
|
|
var state = new OverlapReportState();
|
|
var generation = state.Begin(plate);
|
|
var report = PlateOverlapAnalyzer.Analyze(
|
|
PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), plate.ClampZones.ToArray()));
|
|
plate.ClampZones.Clear();
|
|
Assert.False(state.TryPublish(generation, plate, report));
|
|
Assert.NotEqual(OverlapCheckStatus.Current, state.Status);
|
|
}
|
|
|
|
// C16 (PM 6859db53): a 1e-5 square zone, far below the outline tolerance. A part covering
|
|
// it is still reported (never silently clear); flush or clear parts are not.
|
|
[Theory]
|
|
[InlineData(7.5, true)]
|
|
[InlineData(6.0, false)]
|
|
[InlineData(8.00001, false)]
|
|
[InlineData(3.0, false)]
|
|
public void TinyClampZoneIsJudgedWithoutOutlineCollapse(double x, bool overlaps)
|
|
{
|
|
var tiny = new ClampZone("tiny", new Box(8, 0, 1e-5, 1e-5));
|
|
var parts = new List<Part> { new(Square("p", 2), new Vector(x, 0)) };
|
|
|
|
var report = PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.Capture(parts, new[] { tiny }));
|
|
|
|
Assert.True(report.IsComplete, string.Join("; ", report.Issues.Select(issue => issue.Message)));
|
|
Assert.Equal(overlaps, report.ClampFindings.Any(f => f.PartBName.Contains("tiny")));
|
|
}
|
|
|
|
private static string Describe(PlateOverlapReport report) =>
|
|
string.Join(";", report.Pairs.Select(p => $"{p.PartAId}/{p.PartBId}:{p.Area:R}"))
|
|
+ "|"
|
|
+ string.Join(";", report.ClampFindings.Select(f => $"{f.PartAId}/{f.PartBName}:{f.Area:R}"));
|
|
}
|