mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 01:21:55 -04:00
feat(diagnostics): warn on cutoffs and rapids crossing clamps
This commit is contained in:
1 parent
3014ab3480
commit
d7187f65c7
14 files changed
+376
-21
No files matched your search
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Diagnostics;
|
||||
|
||||
/// <summary>A nominal manual cut-off line meeting an active, physical clamp footprint.</summary>
|
||||
public sealed record CutOffClampFinding(int CutOffIndex, string ClampId, Vector Location);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only axis-aligned clamp contact checks. Nominal cut-off definitions are checked
|
||||
/// even if their current regenerated program is stale or trimmed by nearby parts.
|
||||
/// A removed clamp is absent from Plate.ClampZones and therefore cannot be reported.
|
||||
/// </summary>
|
||||
public static class ClampCutDiagnostics
|
||||
{
|
||||
public static IReadOnlyList<CutOffClampFinding> Find(Plate plate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plate);
|
||||
var findings = new List<CutOffClampFinding>();
|
||||
if (plate.ClampZones.Count == 0 || plate.CutOffs.Count == 0)
|
||||
return findings.AsReadOnly();
|
||||
var sheet = plate.BoundingBox(false);
|
||||
for (var index = 0; index < plate.CutOffs.Count; index++)
|
||||
{
|
||||
var cut = plate.CutOffs[index];
|
||||
if (cut == null || !Enum.IsDefined(cut.Axis))
|
||||
throw new ArgumentException("Cut-off has an invalid axis.", nameof(plate));
|
||||
var start = cut.Axis == CutOffAxis.Vertical
|
||||
? new Vector(cut.Position.X, cut.StartLimit ?? sheet.Bottom)
|
||||
: new Vector(cut.StartLimit ?? sheet.Left, cut.Position.Y);
|
||||
var end = cut.Axis == CutOffAxis.Vertical
|
||||
? new Vector(cut.Position.X, cut.EndLimit ?? sheet.Top)
|
||||
: new Vector(cut.EndLimit ?? sheet.Right, cut.Position.Y);
|
||||
foreach (var zone in plate.ClampZones)
|
||||
if (Intersects(start, end, zone.Bounds, out var location))
|
||||
findings.Add(new CutOffClampFinding(index, zone.Id, location));
|
||||
}
|
||||
return findings.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>True for contact with the whole rectangle, including a boundary touch.</summary>
|
||||
public static bool Intersects(Vector from, Vector to, Box box, out Vector location)
|
||||
{
|
||||
location = default;
|
||||
if (box == null || !Finite(from) || !Finite(to) || !Finite(box.Location)
|
||||
|| !double.IsFinite(box.Right) || !double.IsFinite(box.Top)
|
||||
|| box.Length <= 0 || box.Width <= 0)
|
||||
throw new ArgumentException("Clamp contact geometry must be finite and nonempty.");
|
||||
var dx = to.X - from.X;
|
||||
var dy = to.Y - from.Y;
|
||||
if (!double.IsFinite(dx) || !double.IsFinite(dy)
|
||||
|| !double.IsFinite(from.X - box.Left) || !double.IsFinite(box.Right - from.X)
|
||||
|| !double.IsFinite(from.Y - box.Bottom) || !double.IsFinite(box.Top - from.Y))
|
||||
throw new ArgumentException("Clamp contact motion is too large.");
|
||||
var enter = 0.0;
|
||||
var exit = 1.0;
|
||||
if (!Clip(-dx, from.X - box.Left, ref enter, ref exit)
|
||||
|| !Clip(dx, box.Right - from.X, ref enter, ref exit)
|
||||
|| !Clip(-dy, from.Y - box.Bottom, ref enter, ref exit)
|
||||
|| !Clip(dy, box.Top - from.Y, ref enter, ref exit))
|
||||
return false;
|
||||
var midpoint = (enter + exit) * 0.5;
|
||||
location = new Vector(from.X + dx * midpoint, from.Y + dy * midpoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Clip(double p, double q, ref double enter, ref double exit)
|
||||
{
|
||||
if (p == 0)
|
||||
return q >= 0;
|
||||
var ratio = q / p;
|
||||
if (p < 0)
|
||||
enter = System.Math.Max(enter, ratio);
|
||||
else
|
||||
exit = System.Math.Min(exit, ratio);
|
||||
return enter <= exit;
|
||||
}
|
||||
|
||||
private static bool Finite(Vector value) => double.IsFinite(value.X) && double.IsFinite(value.Y);
|
||||
}
|
||||
@@ -12,10 +12,13 @@ public sealed class OverlapGeometryStamp
|
||||
private readonly Plate plate;
|
||||
private readonly Entry[] entries;
|
||||
private readonly (string Id, long X, long Y, long Length, long Width)[] zones;
|
||||
private readonly (CutOff Definition, CutOffAxis Axis, long X, long Y, long? Start, long? End)[] cutoffs;
|
||||
private readonly (long Width, long Length, int Quadrant) sheet;
|
||||
|
||||
private OverlapGeometryStamp(Plate plate)
|
||||
{
|
||||
this.plate = plate;
|
||||
sheet = (Bits(plate.Size.Width), Bits(plate.Size.Length), plate.Quadrant);
|
||||
entries = new Entry[plate.Parts.Count];
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
entries[i] = new Entry(plate.Parts[i]);
|
||||
@@ -26,8 +29,16 @@ public sealed class OverlapGeometryStamp
|
||||
var box = zone.Bounds;
|
||||
zones[i] = (zone.Id, Bits(box.X), Bits(box.Y), Bits(box.Length), Bits(box.Width));
|
||||
}
|
||||
cutoffs = new (CutOff, CutOffAxis, long, long, long?, long?)[plate.CutOffs.Count];
|
||||
for (var i = 0; i < cutoffs.Length; i++)
|
||||
cutoffs[i] = CutOffState(plate.CutOffs[i]);
|
||||
}
|
||||
|
||||
private static (CutOff, CutOffAxis, long, long, long?, long?) CutOffState(CutOff cut) =>
|
||||
(cut, cut.Axis, Bits(cut.Position.X), Bits(cut.Position.Y),
|
||||
cut.StartLimit.HasValue ? Bits(cut.StartLimit.Value) : null,
|
||||
cut.EndLimit.HasValue ? Bits(cut.EndLimit.Value) : null);
|
||||
|
||||
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
|
||||
|
||||
public static OverlapGeometryStamp Capture(Plate plate) => new(plate);
|
||||
@@ -46,9 +57,20 @@ public sealed class OverlapGeometryStamp
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool SameCutOffs(Plate current)
|
||||
{
|
||||
if (!ReferenceEquals(plate, current) || current.CutOffs.Count != cutoffs.Length
|
||||
|| sheet != (Bits(current.Size.Width), Bits(current.Size.Length), current.Quadrant))
|
||||
return false;
|
||||
for (var i = 0; i < cutoffs.Length; i++)
|
||||
if (cutoffs[i] != CutOffState(current.CutOffs[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Matches(Plate current)
|
||||
{
|
||||
if (current.Parts.Count != entries.Length || !SameZones(current))
|
||||
if (current.Parts.Count != entries.Length || !SameZones(current) || !SameCutOffs(current))
|
||||
return false;
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
if (!entries[i].Matches(current.Parts[i]))
|
||||
|
||||
@@ -21,17 +21,22 @@ public sealed class OverlapReportState
|
||||
public PlateOverlapReport Report { get; private set; }
|
||||
/// <summary>Known overlap pairs safe to draw, even while the full layout needs a recheck.</summary>
|
||||
public IReadOnlyList<PlateOverlapPair> DisplayPairs { get; private set; } = Array.Empty<PlateOverlapPair>();
|
||||
public IReadOnlyList<CutOffClampFinding> DisplayCutOffClampFindings { get; private set; } =
|
||||
Array.Empty<CutOffClampFinding>();
|
||||
public bool IsRunning => Status == OverlapCheckStatus.Checking;
|
||||
|
||||
public string Message => Status switch
|
||||
{
|
||||
OverlapCheckStatus.Checking => "Checking overlaps…",
|
||||
OverlapCheckStatus.Current => Report.ClampFindings.Count == 0
|
||||
OverlapCheckStatus.Current => Report.ClampFindings.Count == 0 && Report.CutOffClampFindings.Count == 0
|
||||
? (Report.Pairs.Count == 0 ? "No material overlaps detected"
|
||||
: $"Overlaps: {Report.Pairs.Count} pairs")
|
||||
: $"Overlaps: {Report.Pairs.Count} pairs, {Report.ClampFindings.Count} clamps",
|
||||
: $"Overlaps: {Report.Pairs.Count} pairs, {Report.ClampFindings.Count} clamps, "
|
||||
+ $"{Report.CutOffClampFindings.Count} cut-off/clamp contacts",
|
||||
OverlapCheckStatus.Incomplete => $"Overlap check incomplete: {Report.Pairs.Count} overlapping pairs; "
|
||||
+ (Report.ClampFindings.Count == 0 ? "" : $"{Report.ClampFindings.Count} clamp findings; ")
|
||||
+ (Report.CutOffClampFindings.Count == 0 ? ""
|
||||
: $"{Report.CutOffClampFindings.Count} cut-off/clamp contacts; ")
|
||||
+ $"{uncheckedPartCount} parts could not be checked",
|
||||
OverlapCheckStatus.Failed => "Overlap check failed — run Check Overlaps again",
|
||||
OverlapCheckStatus.Canceled => "Overlap check canceled",
|
||||
@@ -60,6 +65,7 @@ public sealed class OverlapReportState
|
||||
Report = report;
|
||||
DisplayPairs = report.ClampFindings.Count == 0 ? report.Pairs
|
||||
: report.Pairs.Concat(report.ClampFindings).ToList().AsReadOnly();
|
||||
DisplayCutOffClampFindings = report.CutOffClampFindings;
|
||||
displayStamp = stamp;
|
||||
observedDisplayStamp = stamp;
|
||||
uncheckedPartCount = CountUncheckedParts(report.Issues);
|
||||
@@ -111,11 +117,13 @@ public sealed class OverlapReportState
|
||||
return;
|
||||
var unchanged = displayStamp.UnchangedSlots(plate);
|
||||
var sameZones = displayStamp.SameZones(plate);
|
||||
if (!sameZones || !displayStamp.SameCutOffs(plate))
|
||||
DisplayCutOffClampFindings = Array.Empty<CutOffClampFinding>();
|
||||
var retained = DisplayPairs.Where(pair => unchanged[pair.PartAId]
|
||||
&& (pair.PartBId < 0 ? sameZones : unchanged[pair.PartBId])).ToList();
|
||||
if (retained.Count != DisplayPairs.Count)
|
||||
DisplayPairs = retained.AsReadOnly();
|
||||
if (DisplayPairs.Count == 0)
|
||||
if (DisplayPairs.Count == 0 && DisplayCutOffClampFindings.Count == 0)
|
||||
ClearDisplayPairs();
|
||||
else
|
||||
observedDisplayStamp = OverlapGeometryStamp.Capture(plate);
|
||||
@@ -124,6 +132,7 @@ public sealed class OverlapReportState
|
||||
private void ClearDisplayPairs()
|
||||
{
|
||||
DisplayPairs = Array.Empty<PlateOverlapPair>();
|
||||
DisplayCutOffClampFindings = Array.Empty<CutOffClampFinding>();
|
||||
displayStamp = null;
|
||||
observedDisplayStamp = null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,23 @@ public static class PlateOverlapAnalyzer
|
||||
{
|
||||
public const double ChordTolerance = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// Capture a whole plate including active clamps and nominal cut-off definitions.
|
||||
/// Use this entry for the desktop overlap check so manual cut-off contacts
|
||||
/// cannot be mistaken for a clear plate merely because cut-offs are not material.
|
||||
/// </summary>
|
||||
public static PlateOverlapSnapshot CapturePlate(Plate plate, OverlapMaterialCache cache = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plate);
|
||||
var material = Capture(plate.Parts.ToArray(), plate.ClampZones.ToArray(),
|
||||
cache ?? new OverlapMaterialCache(), cancellationToken);
|
||||
var cutoffs = ClampCutDiagnostics.Find(plate);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new PlateOverlapSnapshot(material.Parts.ToList(), material.Issues.ToList(),
|
||||
material.Clamps.ToList(), cutoffs.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures poses and converts each distinct clean source program to owned entities once.
|
||||
/// Inputs must not change during capture. Later analysis never reads live domain objects.
|
||||
@@ -226,7 +243,8 @@ public static class PlateOverlapAnalyzer
|
||||
return new PlateOverlapReport(pairs.OrderBy(pair => pair.PartAId)
|
||||
.ThenBy(pair => pair.PartBId).ToList(), issues.OrderBy(issue => issue.PartAId)
|
||||
.ThenBy(issue => issue.PartBId).ToList(), snapshot, clampFindings
|
||||
.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToList());
|
||||
.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToList(),
|
||||
snapshot.CutOffClampFindings.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,12 +8,14 @@ namespace OpenNest.Diagnostics;
|
||||
public sealed class PlateOverlapReport
|
||||
{
|
||||
internal PlateOverlapReport(List<PlateOverlapPair> pairs, List<PlateOverlapIssue> issues,
|
||||
PlateOverlapSnapshot snapshot, List<PlateOverlapPair> clampFindings = null)
|
||||
PlateOverlapSnapshot snapshot, List<PlateOverlapPair> clampFindings = null,
|
||||
List<CutOffClampFinding> cutOffClampFindings = null)
|
||||
{
|
||||
Pairs = pairs.AsReadOnly();
|
||||
Issues = issues.AsReadOnly();
|
||||
Snapshot = snapshot;
|
||||
ClampFindings = (clampFindings ?? new List<PlateOverlapPair>()).AsReadOnly();
|
||||
CutOffClampFindings = (cutOffClampFindings ?? new List<CutOffClampFinding>()).AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>The owned input this report was computed from; the baseline for incremental rechecks.</summary>
|
||||
@@ -28,6 +30,8 @@ public sealed class PlateOverlapReport
|
||||
/// are reported like other overlap findings; they do not change <see cref="IsComplete"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PlateOverlapPair> ClampFindings { get; }
|
||||
/// <summary>Nominal cut-off/clamp contacts. Lines have no material area or part pair.</summary>
|
||||
public IReadOnlyList<CutOffClampFinding> CutOffClampFindings { get; }
|
||||
|
||||
public bool IsComplete => Issues.Count == 0;
|
||||
public double ChordTolerance => PlateOverlapAnalyzer.ChordTolerance;
|
||||
@@ -112,11 +116,12 @@ public sealed record PlateOverlapIssue(int PartAId, int? PartBId, string Message
|
||||
public sealed class PlateOverlapSnapshot
|
||||
{
|
||||
internal PlateOverlapSnapshot(List<CapturedOverlapPart> parts, List<PlateOverlapIssue> issues,
|
||||
List<CapturedClamp> clamps = null)
|
||||
List<CapturedClamp> clamps = null, List<CutOffClampFinding> cutOffClampFindings = null)
|
||||
{
|
||||
Parts = parts.AsReadOnly();
|
||||
Issues = issues.AsReadOnly();
|
||||
Clamps = (clamps ?? new List<CapturedClamp>()).AsReadOnly();
|
||||
CutOffClampFindings = (cutOffClampFindings ?? new List<CutOffClampFinding>()).AsReadOnly();
|
||||
}
|
||||
|
||||
internal IReadOnlyList<CapturedOverlapPart> Parts { get; }
|
||||
@@ -124,6 +129,7 @@ public sealed class PlateOverlapSnapshot
|
||||
|
||||
/// <summary>Owned clamp keep-out rectangles in plate coordinates, capture order.</summary>
|
||||
internal IReadOnlyList<CapturedClamp> Clamps { get; }
|
||||
internal IReadOnlyList<CutOffClampFinding> CutOffClampFindings { get; }
|
||||
}
|
||||
|
||||
internal sealed record CapturedOverlapPart(int Id, string Name, OverlapSource Source,
|
||||
|
||||
@@ -56,6 +56,30 @@ public static class PostVerificationAnalyzer
|
||||
}
|
||||
var materialParts = new List<Part>();
|
||||
var indices = new List<int>();
|
||||
try
|
||||
{
|
||||
foreach (var contact in ClampCutDiagnostics.Find(plate))
|
||||
{
|
||||
var cut = plate.CutOffs[contact.CutOffIndex];
|
||||
var partIndex = -1;
|
||||
for (var partSlot = 0; partSlot < plate.Parts.Count; partSlot++)
|
||||
if (ReferenceEquals(plate.Parts[partSlot]?.BaseDrawing, cut.Drawing))
|
||||
{
|
||||
partIndex = partSlot;
|
||||
break;
|
||||
}
|
||||
findings.Add(new(PostVerificationKind.Overlap, plateNumber,
|
||||
partIndex < 0 ? null : partIndex + 1, null,
|
||||
$"Manual cut-off {contact.CutOffIndex + 1} crosses active clamp '{contact.ClampId}'. "
|
||||
+ "Review the clamp and cut before posting; this warning is not a repair.")
|
||||
{ Location = contact.Location, ClampId = contact.ClampId });
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (IsInvalid(exception))
|
||||
{
|
||||
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, null, null,
|
||||
"Clamp/cut-off check: " + exception.Message));
|
||||
}
|
||||
var obstacles = new ReleasedContourState();
|
||||
Vector? position = startPoint;
|
||||
for (var index = 0; index < plate.Parts.Count; index++)
|
||||
@@ -95,6 +119,27 @@ public static class PostVerificationAnalyzer
|
||||
&& move.Curve.Length > PostVerificationGeometry.Epsilon))
|
||||
Incomplete("Placed program has no cutting contour motions for this drawing.");
|
||||
obstacles.AnalyzeMoves(moves, cutoff, findings, plateNumber, partNumber, cancellationToken);
|
||||
if (plate.ClampZones.Count != 0)
|
||||
foreach (var move in moves)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!move.Rapid || move.Start is not { } from
|
||||
|| from.X == move.End.X && from.Y == move.End.Y)
|
||||
continue;
|
||||
foreach (var zone in plate.ClampZones)
|
||||
if (ClampCutDiagnostics.Intersects(from, move.End, zone.Bounds, out var contact))
|
||||
findings.Add(new(PostVerificationKind.RapidCrossing, plateNumber,
|
||||
partNumber, null,
|
||||
$"Direct XY rapid crosses active clamp '{zone.Id}'. "
|
||||
+ "Head height is not modeled; review the posted route and clamp setup.")
|
||||
{
|
||||
Location = (from + move.End) * 0.5,
|
||||
RapidStart = from,
|
||||
RapidEnd = move.End,
|
||||
ContactPoints = new[] { contact },
|
||||
ClampId = zone.Id
|
||||
});
|
||||
}
|
||||
position = moves[^1].End;
|
||||
}
|
||||
catch (Exception exception) when (IsInvalid(exception))
|
||||
@@ -110,11 +155,16 @@ public static class PostVerificationAnalyzer
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var overlap = PlateOverlapAnalyzer.Analyze(
|
||||
PlateOverlapAnalyzer.Capture(materialParts, cancellationToken), cancellationToken);
|
||||
PlateOverlapAnalyzer.Capture(materialParts, plate.ClampZones.ToArray(), cancellationToken),
|
||||
cancellationToken);
|
||||
foreach (var pair in overlap.Pairs)
|
||||
findings.Add(new(PostVerificationKind.Overlap, plateNumber, indices[pair.PartAId],
|
||||
indices[pair.PartBId], "Clean drawing material overlaps (holes subtracted).")
|
||||
{ Location = pair.Centroid, Overlap = pair });
|
||||
foreach (var pair in overlap.ClampFindings)
|
||||
findings.Add(new(PostVerificationKind.Overlap, plateNumber, indices[pair.PartAId],
|
||||
null, $"Clean drawing material overlaps {pair.PartBName} (holes subtracted).")
|
||||
{ Location = pair.Centroid, Overlap = pair });
|
||||
foreach (var issue in overlap.Issues)
|
||||
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, indices[issue.PartAId],
|
||||
issue.PartBId is { } other ? indices[other] : null, "Overlap check: " + issue.Message));
|
||||
|
||||
@@ -24,6 +24,8 @@ public sealed record PostVerificationFinding(PostVerificationKind Kind, int Plat
|
||||
/// <summary>Rapid boundary contacts in travel order; collinear contact spans use their endpoints.</summary>
|
||||
public IReadOnlyList<Vector> ContactPoints { get; init; } = Array.Empty<Vector>();
|
||||
public PlateOverlapPair Overlap { get; init; }
|
||||
/// <summary>Active clamp contacted by a cut-off or direct XY rapid, if applicable.</summary>
|
||||
public string ClampId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Owned, immutable findings. Consent is evaluated afresh, never stored.</summary>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Diagnostics;
|
||||
|
||||
public class ClampCutDiagnosticsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(CutOffAxis.Vertical, 10.5, 0, 1, true)]
|
||||
[InlineData(CutOffAxis.Vertical, 10.5, 2, 4, false)]
|
||||
[InlineData(CutOffAxis.Vertical, 14, 0, 2, false)]
|
||||
[InlineData(CutOffAxis.Horizontal, 0.25, 9, 13, true)]
|
||||
[InlineData(CutOffAxis.Horizontal, 2, 9, 13, false)]
|
||||
[InlineData(CutOffAxis.Horizontal, 0.25, 12, 13, false)]
|
||||
public void NominalLimitsAndBothAxesDetermineContact(CutOffAxis axis, double coordinate,
|
||||
double start, double end, bool crosses)
|
||||
{
|
||||
var plate = new Plate(24, 48);
|
||||
plate.ClampZones.Add(new ClampZone("front", new Box(10, 0, 1.5, 0.625)));
|
||||
plate.CutOffs.Add(new CutOff(axis == CutOffAxis.Vertical
|
||||
? new Vector(coordinate, 0) : new Vector(0, coordinate), axis)
|
||||
{ StartLimit = start, EndLimit = end });
|
||||
var findings = ClampCutDiagnostics.Find(plate);
|
||||
Assert.Equal(crosses, findings.Count != 0);
|
||||
if (crosses)
|
||||
Assert.Equal("front", Assert.Single(findings).ClampId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SegmentChecksPhysicalFootprintNotInfiniteLineOrPartSpacing()
|
||||
{
|
||||
var zone = new Box(10, 0, 1.5, 0.625);
|
||||
Assert.True(ClampCutDiagnostics.Intersects(new Vector(9, 0.3), new Vector(12, 0.3),
|
||||
zone, out var contact));
|
||||
Assert.InRange(contact.X, 10, 11.5);
|
||||
Assert.False(ClampCutDiagnostics.Intersects(new Vector(9, 3), new Vector(12, 3),
|
||||
zone, out _));
|
||||
Assert.False(ClampCutDiagnostics.Intersects(new Vector(9, 0.3), new Vector(9.9, 0.3),
|
||||
zone, out _));
|
||||
Assert.Throws<ArgumentException>(() => ClampCutDiagnostics.Intersects(
|
||||
new Vector(double.NaN, 0), new Vector(12, 0), zone, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyActiveClampsAreReported()
|
||||
{
|
||||
var plate = new Plate(24, 48);
|
||||
plate.ClampZones.Add(new ClampZone("front", new Box(10, 0, 1.5, 0.625)));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(10.5, 0), CutOffAxis.Vertical));
|
||||
Assert.Single(ClampCutDiagnostics.Find(plate));
|
||||
plate.ClampZones.Clear(); // Removed on this sheet; no longer an obstacle.
|
||||
Assert.Empty(ClampCutDiagnostics.Find(plate));
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,28 @@ public class PlateOverlapAnalyzerClampZoneTests
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -98,6 +98,53 @@ public class PostVerificationAnalyzerTests
|
||||
Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualCutoffAndRapidCrossingClampRequireFreshPostingConsent()
|
||||
{
|
||||
var nest = NestWith(Rectangle(50, 20, 4));
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
plate.ClampZones.Add(new ClampZone("front-3", new Box(10, 0, 1.5, 0.625)));
|
||||
var cutoff = new CutOff(new Vector(10.5, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
var report = PostVerificationAnalyzer.Analyze(nest);
|
||||
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.Overlap
|
||||
&& f.Message.Contains("front-3") && f.Message.Contains("cut-off") && f.PlateNumber == 1);
|
||||
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.RapidCrossing
|
||||
&& f.Message.Contains("front-3") && f.RapidStart.HasValue && f.RapidEnd.HasValue);
|
||||
Assert.False(report.CanPost(false));
|
||||
Assert.True(report.CanPost(true));
|
||||
Assert.False(PostVerificationAnalyzer.Analyze(nest).CanPost(false));
|
||||
plate.ClampZones.Clear();
|
||||
var withoutClamps = PostVerificationAnalyzer.Analyze(nest);
|
||||
Assert.DoesNotContain(withoutClamps.Findings, f => f.Message.Contains("front-3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidManualCutoffGeometryIsIncompleteNotClear()
|
||||
{
|
||||
var nest = NestWith(Rectangle(40, 20, 4));
|
||||
var plate = nest.Plates[0];
|
||||
plate.ClampZones.Add(new ClampZone("front", new Box(10, 0, 1.5, 0.625)));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(double.NaN, 0), CutOffAxis.Vertical));
|
||||
var report = PostVerificationAnalyzer.Analyze(nest);
|
||||
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.Incomplete
|
||||
&& f.Message.Contains("Clamp/cut-off check"));
|
||||
Assert.False(report.CanPost(false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartMaterialOnClampAppearsInPrePostOverlapFindings()
|
||||
{
|
||||
var nest = NestWith(Rectangle(10, 0, 4));
|
||||
nest.Plates[0].ClampZones.Add(new ClampZone("front-1", new Box(10, 0, 1.5, 0.625)));
|
||||
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings.Where(f =>
|
||||
f.Kind == PostVerificationKind.Overlap && f.Message.Contains("front-1")));
|
||||
Assert.Equal(1, finding.PartNumber);
|
||||
Assert.Null(finding.OtherPartNumber);
|
||||
Assert.NotNull(finding.Overlap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScribeAndCutoff_AreExemptFromOverlapAndLeadInChecks()
|
||||
{
|
||||
|
||||
@@ -148,8 +148,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
try
|
||||
{
|
||||
// Do not include PreviewManager parts. Capture owns clean geometry and stable names.
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(),
|
||||
plate.ClampZones.ToArray(), materialCache, token);
|
||||
var snapshot = PlateOverlapAnalyzer.CapturePlate(plate, materialCache, token);
|
||||
var previous = baseline;
|
||||
var analyze = Analyze ?? ((input, cancel) => PlateOverlapAnalyzer.Analyze(input, previous, cancel));
|
||||
var report = await Task.Run(() => analyze(snapshot, token), token);
|
||||
@@ -381,6 +380,8 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
if (ShowsCentroids)
|
||||
DrawCentroids(graphics);
|
||||
}
|
||||
if (state.DisplayMode != OverlapDisplayMode.Off)
|
||||
DrawCutOffClampContacts(graphics);
|
||||
DrawStateLabel(graphics);
|
||||
}
|
||||
|
||||
@@ -598,6 +599,32 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCutOffClampContacts(Graphics graphics)
|
||||
{
|
||||
var dpi = view.DeviceDpi / 96f;
|
||||
using var halo = new Pen(Color.White, 4 * dpi);
|
||||
using var warning = new Pen(Color.DarkRed, 2 * dpi);
|
||||
foreach (var finding in state.DisplayCutOffClampFindings)
|
||||
{
|
||||
var point = view.PointWorldToGraph(finding.Location);
|
||||
if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
|
||||
continue;
|
||||
foreach (var pen in new[] { halo, warning })
|
||||
{
|
||||
graphics.DrawLine(pen, point.X - 8 * dpi, point.Y - 8 * dpi,
|
||||
point.X + 8 * dpi, point.Y + 8 * dpi);
|
||||
graphics.DrawLine(pen, point.X - 8 * dpi, point.Y + 8 * dpi,
|
||||
point.X + 8 * dpi, point.Y - 8 * dpi);
|
||||
}
|
||||
var label = $"Cut-off {finding.CutOffIndex + 1} / {finding.ClampId}";
|
||||
var size = graphics.MeasureString(label, view.Font);
|
||||
var x = point.X + 10 * dpi;
|
||||
var y = point.Y - size.Height;
|
||||
graphics.FillRectangle(Brushes.White, x, y, size.Width, size.Height);
|
||||
graphics.DrawString(label, view.Font, Brushes.DarkRed, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawStateLabel(Graphics graphics)
|
||||
{
|
||||
var saved = graphics.Save();
|
||||
|
||||
@@ -302,7 +302,11 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
view.ShowValidationFinding(finding with { Location = location });
|
||||
var hint = finding.Kind switch
|
||||
{
|
||||
PostVerificationKind.Overlap when finding.ClampId != null =>
|
||||
"The ring marks a nominal cut-off contacting a clamp; no material-overlap area is implied.",
|
||||
PostVerificationKind.Overlap => "Shaded material is the overlap. The ring marks its area centroid, which can lie between separate patches.",
|
||||
PostVerificationKind.RapidCrossing when finding.ClampId != null =>
|
||||
"The crosshair marks the rapid's contact with the clamp. Head height is not modeled.",
|
||||
PostVerificationKind.RapidCrossing => finding.ContactPoints.Count > 0
|
||||
? "Crosshairs mark contacts with the completed contour. The pulsing dashed orange line shows the rapid."
|
||||
: "The pulsing dashed orange line shows the rapid inside the completed contour; there is no boundary contact point to mark.",
|
||||
|
||||
@@ -98,8 +98,8 @@ vertices cannot mutate the snapshot, report, or live geometry.
|
||||
## Capture once, analyze off-thread
|
||||
|
||||
```csharp
|
||||
// UI thread, while parts/drawings are stable:
|
||||
var snapshot = PlateOverlapAnalyzer.Capture(parts, plate.ClampZones.ToArray(), cancellationToken);
|
||||
// UI thread, while the plate and drawings are stable:
|
||||
var snapshot = PlateOverlapAnalyzer.CapturePlate(plate, cancellationToken: cancellationToken);
|
||||
|
||||
// Worker thread, no access to live Part/Drawing/Program objects:
|
||||
var report = await Task.Run(
|
||||
@@ -159,6 +159,14 @@ triangulation, or `Collision.Check` call is not internally interruptible.
|
||||
A report with any issue has `IsComplete == false`, even if `Pairs` is empty.
|
||||
- No part, drawing, quantity, pose, cutting state, or selection is modified.
|
||||
|
||||
For a full plate, `PlateOverlapAnalyzer.CapturePlate(plate)` also captures active
|
||||
clamps and nominal manual cut-off definitions. `CutOffClampFindings` is a separate
|
||||
list of zero-area line/rectangle contacts, not `Pairs` or material overlap. The
|
||||
view marks these with an X and a cut-off number/clamp ID, including contacts whose
|
||||
current regenerated program has been trimmed. Removed clamps produce no contact.
|
||||
Editing definitions, sheet size/quadrant or zones stales the result; posting runs
|
||||
its own fresh check with per-attempt consent.
|
||||
|
||||
## Interpretation and limits
|
||||
|
||||
Regions are the kernel's convex, hole-subtracted fragments, not merged connected
|
||||
|
||||
@@ -52,18 +52,23 @@ The desktop's **Nest > Post > (post processor)** workflow checks every plate aft
|
||||
post settings are accepted and before asking where to save CNC output. The review
|
||||
shows all three check categories, even when there are no findings:
|
||||
|
||||
- **Overlapping parts:** shared material between placed parts, including containment.
|
||||
Holes are subtracted; placing a part inside a sufficiently large cutout is not
|
||||
itself an overlap. This uses clean drawing geometry, not added lead-in/out paths.
|
||||
- **Overlapping parts and clamps:** shared material between placed parts, including containment,
|
||||
and physical material overlapping an active clamp. Holes are subtracted; placing a part inside
|
||||
a sufficiently large cutout is not itself an overlap. The same review also warns if a nominal
|
||||
manual cut-off line crosses an active clamp, even when its regenerated cut is currently trimmed;
|
||||
a line contact has no material-overlap area. Removed clamps on that sheet are not checked.
|
||||
This uses clean drawing geometry, not added lead-in/out paths.
|
||||
- **Missing lead-ins:** cutting contours without an actual, nonzero lead-in in the
|
||||
placed program. Applying a lead-in to only one contour does not clear the other
|
||||
contours. Scribe-only work and scrap cutoff lines do not require lead-ins here.
|
||||
The cutting planner supports direct contour piercing without lead-ins; that does
|
||||
not suppress this pre-post review warning or grant posting consent.
|
||||
- **Rapid crossings:** direct XY moves crossing closed, already-cut, untabbed
|
||||
contours in cutting order. This includes earlier holes within the same part and
|
||||
previously cut parts. A future cut is not an obstacle yet. Actual uncut gaps are
|
||||
respected; enabling tabs in settings alone is not evidence that a gap exists.
|
||||
contours in cutting order, or an active clamp's physical footprint. This includes
|
||||
earlier holes within the same part and previously cut parts. A future cut is not
|
||||
an obstacle yet. Actual uncut gaps are respected; enabling tabs in settings alone
|
||||
is not evidence that a gap exists. Clamp-rapid warnings are conservative: head
|
||||
height/retracts are not modeled, so inspect the emitted route and machine setup.
|
||||
|
||||
Findings identify the plate and part's position in the cutting sequence. Read the
|
||||
scrollable report, cancel to fix the nest, then post again to rerun verification.
|
||||
@@ -120,9 +125,9 @@ change does not silently alter their existing CNC output order.
|
||||
These checks are warnings, not a machine safety certification. They inspect the
|
||||
nest's placed programs and direct XY travel, not a simulation of final NC output.
|
||||
A post processor or controller may change routing, cutting order, lead-ins,
|
||||
retract height, or parking moves. The checks do not model tipping, slats, clamps,
|
||||
actual tab strength, kerf, or safe Z height. Review the generated CNC program and
|
||||
machine setup before cutting, even when this report has no warnings.
|
||||
retract height, or parking moves. They check planar clamp footprints but not clamp
|
||||
height, actual head clearance, tipping, slats or tab strength. Review the generated
|
||||
CNC program and machine setup before cutting, even when this report has no warnings.
|
||||
|
||||
`OpenNest.Core/Diagnostics/PostVerificationAnalyzer` owns the cross-platform logic;
|
||||
its report exposes findings, display text, and `CanPost(risksAcknowledged)`. Use
|
||||
|
||||
Reference in new issue
Block a user