mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 22:24:14 -04:00
Add interactive nest validation with animated issue highlights
This commit is contained in:
1 parent
80c647bf07
commit
bb108e9341
12 files changed
+959
-17
No files matched your search
@@ -68,7 +68,8 @@ public sealed class ReleasedContourState
|
||||
if (PostVerificationGeometry.Crosses(start, move.End, obstacle.Curves, token))
|
||||
findings.Add(new(PostVerificationKind.RapidCrossing, plate, part, obstacle.Part,
|
||||
$"Direct XY rapid crosses or touches completed untabbed contour {obstacle.Contour} " +
|
||||
$"of part {obstacle.Part}."));
|
||||
$"of part {obstacle.Part}.")
|
||||
{ Location = (start + move.End) * 0.5, RapidStart = start, RapidEnd = move.End });
|
||||
}
|
||||
}
|
||||
else if (move.Layer == LayerType.Leadin)
|
||||
@@ -94,7 +95,8 @@ public sealed class ReleasedContourState
|
||||
contourNumber++;
|
||||
if (!cutoff && !hasLead)
|
||||
findings.Add(new(PostVerificationKind.MissingLeadIn, plate, part, null,
|
||||
$"Cutting contour {contourNumber} has no nonzero placed lead-in motion."));
|
||||
$"Cutting contour {contourNumber} has no nonzero placed lead-in motion.")
|
||||
{ Location = move.Curve.Start });
|
||||
hasLead = false;
|
||||
// A rapid can pause/reposition without leaving any material gap.
|
||||
// Retain already-cut fragments, but do not turn them into obstacles
|
||||
|
||||
@@ -113,7 +113,8 @@ public static class PostVerificationAnalyzer
|
||||
PlateOverlapAnalyzer.Capture(materialParts, 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)."));
|
||||
indices[pair.PartBId], "Clean drawing material overlaps (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));
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Diagnostics;
|
||||
|
||||
@@ -15,7 +16,13 @@ public enum PostVerificationKind
|
||||
|
||||
/// <summary>Plate and part numbers are one-based; plate zero denotes a whole-post limitation.</summary>
|
||||
public sealed record PostVerificationFinding(PostVerificationKind Kind, int PlateNumber,
|
||||
int? PartNumber, int? OtherPartNumber, string Message);
|
||||
int? PartNumber, int? OtherPartNumber, string Message)
|
||||
{
|
||||
public Vector? Location { get; init; }
|
||||
public Vector? RapidStart { get; init; }
|
||||
public Vector? RapidEnd { get; init; }
|
||||
public PlateOverlapPair Overlap { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Owned, immutable findings. Consent is evaluated afresh, never stored.</summary>
|
||||
public sealed class PostVerificationReport
|
||||
@@ -29,10 +36,12 @@ public sealed class PostVerificationReport
|
||||
public bool HasWarnings => Findings.Count != 0;
|
||||
public bool CanPost(bool risksAcknowledged) => !HasWarnings || risksAcknowledged;
|
||||
|
||||
public string ToDisplayText()
|
||||
public string ToDisplayText() => ToDisplayText("Pre-post verification");
|
||||
|
||||
public string ToDisplayText(string title)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
text.AppendLine("Pre-post verification");
|
||||
text.AppendLine(title);
|
||||
var incomplete = Findings.Any(finding => finding.Kind == PostVerificationKind.Incomplete);
|
||||
Summary(PostVerificationKind.Overlap, "Overlap");
|
||||
Summary(PostVerificationKind.MissingLeadIn, "Missing lead-ins");
|
||||
|
||||
@@ -36,6 +36,9 @@ public class PostVerificationAnalyzerTests
|
||||
var report = Analyze(Cutoff(30, 30), a, Scribe(40, 40), b);
|
||||
var overlap = Assert.Single(Find(report, PostVerificationKind.Overlap));
|
||||
Assert.Equal((1, 2, 4), (overlap.PlateNumber, overlap.PartNumber, overlap.OtherPartNumber));
|
||||
Assert.NotNull(overlap.Overlap);
|
||||
Assert.Equal(new Vector(1.5, 1.5), overlap.Location);
|
||||
Assert.Equal(1, overlap.Overlap.Area, 6);
|
||||
Assert.True(report.HasWarnings);
|
||||
Assert.False(report.CanPost(false));
|
||||
Assert.True(report.CanPost(true));
|
||||
@@ -58,7 +61,8 @@ public class PostVerificationAnalyzerTests
|
||||
{
|
||||
var part = Rectangle(0, 0, 4);
|
||||
part.HasManualLeadIns = true;
|
||||
Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
|
||||
var finding = Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
|
||||
Assert.Equal(Vector.Zero, finding.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -105,8 +109,11 @@ public class PostVerificationAnalyzerTests
|
||||
public void Rapid_ThroughPreviouslyCutPartWarns()
|
||||
{
|
||||
var report = Analyze(Rectangle(0, 0, 4), Scribe(-2, 2), Scribe(6, 2));
|
||||
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f =>
|
||||
f.PartNumber == 3 && f.OtherPartNumber == 1);
|
||||
var crossing = Assert.Single(Find(report, PostVerificationKind.RapidCrossing).Where(f =>
|
||||
f.PartNumber == 3 && f.OtherPartNumber == 1));
|
||||
Assert.NotNull(crossing.RapidStart);
|
||||
Assert.NotNull(crossing.RapidEnd);
|
||||
Assert.Equal((crossing.RapidStart.Value + crossing.RapidEnd.Value) * 0.5, crossing.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -169,8 +176,11 @@ public class PostVerificationAnalyzerTests
|
||||
public void Rapid_CutoffTravelStillCrossesEarlierRealCuts()
|
||||
{
|
||||
var report = Analyze(Rectangle(0, 0, 4), Scribe(-2, 2), Cutoff(6, 2));
|
||||
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f =>
|
||||
f.PartNumber == 3 && f.OtherPartNumber == 1);
|
||||
var crossing = Assert.Single(Find(report, PostVerificationKind.RapidCrossing).Where(f =>
|
||||
f.PartNumber == 3 && f.OtherPartNumber == 1));
|
||||
Assert.NotNull(crossing.RapidStart);
|
||||
Assert.NotNull(crossing.RapidEnd);
|
||||
Assert.Equal((crossing.RapidStart.Value + crossing.RapidEnd.Value) * 0.5, crossing.Location);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class ValidateNestFormTests
|
||||
{
|
||||
#pragma warning disable xUnit1031 // Completed tasks on a dedicated STA thread.
|
||||
[Fact]
|
||||
public void GridNavigatesMainViewAndRestoresViewportOnDisposal()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("interactive inspection");
|
||||
var program = new CNC.Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(5, 0);
|
||||
program.LineTo(5, 5);
|
||||
program.LineTo(0, 5);
|
||||
program.LineTo(0, 0);
|
||||
nest.CreatePlate().Parts.Add(new Part(new Drawing("first", program)));
|
||||
var second = nest.CreatePlate();
|
||||
second.Parts.Add(new Part(new Drawing("second", program)));
|
||||
second.Parts.Add(new Part(second.Parts[0].BaseDrawing) { Location = new Vector(2, 2) });
|
||||
using var editor = new EditNestForm(nest);
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
var view = editor.PlateView;
|
||||
view.ZoomToArea(-10, -10, 40, 40);
|
||||
var viewport = view.CaptureViewport();
|
||||
view.OverlapDisplay = OverlapDisplayMode.Centroids;
|
||||
using (var form = new ValidateNestForm(editor,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token))))
|
||||
{
|
||||
form.Show(editor);
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.True(form.TopLevel);
|
||||
Assert.False(form.Modal);
|
||||
Assert.True(editor.Enabled);
|
||||
Assert.True(view.Enabled);
|
||||
Assert.True(view.InspectionOnly);
|
||||
var grid = Find<DataGridView>(form, "warningGrid");
|
||||
Assert.True(grid.ReadOnly);
|
||||
Assert.Equal(DataGridViewSelectionMode.FullRowSelect, grid.SelectionMode);
|
||||
var row = grid.Rows.Cast<DataGridViewRow>().Single(row =>
|
||||
row.Tag is PostVerificationFinding { Kind: PostVerificationKind.Overlap });
|
||||
grid.CurrentCell = row.Cells[0];
|
||||
Assert.Same(second, view.Plate);
|
||||
Assert.Equal(1, editor.PlateManager.CurrentIndex);
|
||||
Assert.NotNull(view.ValidationOverlay.Finding.Overlap);
|
||||
Assert.True(view.ValidationOverlay.IsAnimating);
|
||||
using var bitmap = new Bitmap(view.Width, view.Height);
|
||||
view.DrawToBitmap(bitmap, view.ClientRectangle);
|
||||
Assert.Contains("centroid", Find<TextBox>(form, "reportBox").Text);
|
||||
view.SelectAll();
|
||||
var scale = view.ViewScale;
|
||||
Invoke(view, "OnMouseWheel", new MouseEventArgs(MouseButtons.None, 0, 100, 100, 120));
|
||||
Assert.True(view.ViewScale > scale);
|
||||
var pan = view.CaptureViewport();
|
||||
Invoke(view, "OnMouseMove", new MouseEventArgs(MouseButtons.Middle, 0, 120, 110, 0));
|
||||
Assert.NotEqual(pan.Origin, view.CaptureViewport().Origin);
|
||||
Invoke(view, "OnKeyDown", new KeyEventArgs(Keys.Delete));
|
||||
Invoke(view, "ProcessDialogKey", Keys.Right);
|
||||
Invoke(view, "OnMouseDown", new MouseEventArgs(MouseButtons.Middle, 1, 120, 110, 0));
|
||||
Invoke(view, "OnMouseUp", new MouseEventArgs(MouseButtons.Middle, 1, 120, 110, 0));
|
||||
Assert.Equal(2, second.Parts.Count);
|
||||
Assert.All(second.Parts, part => Assert.Equal(0, part.Rotation));
|
||||
}
|
||||
Assert.Same(nest.Plates[0], view.Plate);
|
||||
Assert.Equal(viewport, view.CaptureViewport());
|
||||
Assert.Equal(OverlapDisplayMode.Centroids, view.OverlapDisplay);
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
Assert.False(view.InspectionOnly);
|
||||
Assert.Equal(new Vector(2, 2), second.Parts[1].Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelessCancellationKeepsEditingLockedUntilWorkerStops()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var pending = new TaskCompletionSource<PostVerificationReport>();
|
||||
using var editor = new EditNestForm(new Nest("inspection"));
|
||||
editor.Show();
|
||||
using var form = new ValidateNestForm(editor, (_, _) => pending.Task);
|
||||
form.Show(editor);
|
||||
var validation = form.ValidateAsync();
|
||||
Assert.False(validation.IsCompleted);
|
||||
Assert.True(editor.Enabled);
|
||||
Assert.True(editor.PlateView.Enabled);
|
||||
Assert.True(editor.PlateView.InspectionOnly);
|
||||
form.Close();
|
||||
Assert.True(form.Visible);
|
||||
Assert.True(editor.PlateView.InspectionOnly);
|
||||
pending.SetCanceled();
|
||||
var timeout = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (!form.IsDisposed && timeout.Elapsed < TimeSpan.FromSeconds(5))
|
||||
Application.DoEvents();
|
||||
Assert.True(form.IsDisposed);
|
||||
Assert.False(editor.PlateView.InspectionOnly);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlateSwitchClearsAnimation()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var view = new PlateView();
|
||||
view.ShowValidationFinding(new PostVerificationFinding(PostVerificationKind.MissingLeadIn,
|
||||
1, 1, null, "missing")
|
||||
{ Location = new Vector(3, 4) });
|
||||
Assert.True(view.ValidationOverlay.IsAnimating);
|
||||
view.Plate = new Plate();
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChecksAllPlatesWithoutChangingProgramsOrPlacements()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("PEP inspection");
|
||||
var program = new CNC.Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(5, 0));
|
||||
program.Codes.Add(new LinearMove(5, 5));
|
||||
program.Codes.Add(new LinearMove(0, 5));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
var first = new Part(new Drawing("square", program));
|
||||
var second = new Part(first.BaseDrawing);
|
||||
nest.CreatePlate().Parts.Add(first);
|
||||
nest.CreatePlate().Parts.Add(second);
|
||||
var firstProgram = first.Program;
|
||||
var secondProgram = second.Program;
|
||||
var firstCodes = firstProgram.Codes.ToArray();
|
||||
var location = first.Location;
|
||||
var rotation = first.Rotation;
|
||||
using var form = new ValidateNestForm(nest,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token)));
|
||||
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
|
||||
var report = Find<TextBox>(form, "reportBox");
|
||||
Assert.True(report.ReadOnly);
|
||||
Assert.Contains("Plate 1, part 1", report.Text);
|
||||
Assert.Contains("Plate 2, part 1", report.Text);
|
||||
Assert.DoesNotContain("Post processor", report.Text);
|
||||
Assert.Contains("Warnings found", Find<Label>(form, "summaryLabel").Text);
|
||||
Assert.Same(first, nest.Plates[0].Parts[0]);
|
||||
Assert.Same(second, nest.Plates[1].Parts[0]);
|
||||
Assert.Same(firstProgram, first.Program);
|
||||
Assert.Same(secondProgram, second.Program);
|
||||
Assert.Equal(firstCodes, first.Program.Codes.ToArray());
|
||||
Assert.Equal(location, first.Location);
|
||||
Assert.Equal(rotation, first.Rotation);
|
||||
Assert.Same(Find<Button>(form, "closeButton"), form.AcceptButton);
|
||||
Assert.Same(form.AcceptButton, form.CancelButton);
|
||||
Assert.Empty(form.Controls.Find("postButton", true));
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DistinguishesClearAndIncompleteChecks(bool incomplete)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("inspection");
|
||||
if (incomplete)
|
||||
nest.CreatePlate().Parts.Add(new Part(new Drawing("invalid", new CNC.Program())));
|
||||
using var form = new ValidateNestForm(nest,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token)));
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.Contains(incomplete ? "Validation incomplete" : "No warnings found",
|
||||
Find<Label>(form, "summaryLabel").Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureIsReportedWithoutAnAllClear()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var form = new ValidateNestForm(new Nest("inspection"),
|
||||
(_, _) => Task.FromException<PostVerificationReport>(new InvalidOperationException("test failure")));
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.Contains("Validation failed", Find<Label>(form, "summaryLabel").Text);
|
||||
Assert.Contains("test failure", Find<TextBox>(form, "reportBox").Text);
|
||||
Assert.Equal("Close", Find<Button>(form, "closeButton").Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cancel")]
|
||||
[InlineData("Escape")]
|
||||
[InlineData("Close")]
|
||||
public void RealModalCancellationKeepsOwnerDisabledUntilWorkerStops(string action)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var pending = new TaskCompletionSource<PostVerificationReport>();
|
||||
using var owner = new Form();
|
||||
using var form = new ValidateNestForm(new Nest("inspection"), (_, _) => pending.Task);
|
||||
Exception? failure = null;
|
||||
owner.Show();
|
||||
using var timeout = new System.Windows.Forms.Timer { Interval = 10000 };
|
||||
timeout.Tick += (_, _) =>
|
||||
{
|
||||
failure ??= new TimeoutException("Modal cancellation did not finish.");
|
||||
pending.TrySetCanceled();
|
||||
form.Close();
|
||||
};
|
||||
form.Shown += (_, _) => form.BeginInvoke(new System.Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Assert.False(IsWindowEnabled(owner.Handle));
|
||||
if (action == "Cancel")
|
||||
Find<Button>(form, "closeButton").PerformClick();
|
||||
else if (action == "Escape")
|
||||
Invoke(form, "ProcessDialogKey", Keys.Escape);
|
||||
else
|
||||
form.Close();
|
||||
|
||||
// ShowDialog must stay active while the worker ignores cancellation.
|
||||
Assert.True(form.Visible);
|
||||
Assert.False(IsWindowEnabled(owner.Handle));
|
||||
Assert.False(Find<Button>(form, "closeButton").Enabled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
pending.TrySetCanceled();
|
||||
}
|
||||
}));
|
||||
timeout.Start();
|
||||
var result = form.ShowDialog(owner);
|
||||
timeout.Stop();
|
||||
Assert.Equal(DialogResult.Cancel, result);
|
||||
Assert.True(IsWindowEnabled(owner.Handle));
|
||||
if (failure != null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
});
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowEnabled(IntPtr window);
|
||||
|
||||
private static void Invoke(Control form, string method, params object?[] arguments) =>
|
||||
form.GetType().GetMethod(method, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
||||
.Invoke(form, arguments);
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromSeconds(30), "STA validation test timed out.");
|
||||
|
||||
private static T Find<T>(Form form, string name) where T : Control =>
|
||||
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace OpenNest.Controls
|
||||
{
|
||||
private readonly Timer redrawTimer;
|
||||
private readonly OverlapOverlayController overlapOverlay;
|
||||
private readonly ValidationOverlay validationOverlay;
|
||||
|
||||
private string status;
|
||||
private Plate plate;
|
||||
@@ -102,6 +103,7 @@ namespace OpenNest.Controls
|
||||
public PlateView(ColorScheme colorScheme)
|
||||
{
|
||||
overlapOverlay = new OverlapOverlayController(this);
|
||||
validationOverlay = new ValidationOverlay(this);
|
||||
Plate = new Plate(60, 120);
|
||||
origin = new PointF();
|
||||
parts = new List<LayoutPart>();
|
||||
@@ -151,6 +153,8 @@ namespace OpenNest.Controls
|
||||
|
||||
public ColorScheme ColorScheme { get; set; }
|
||||
|
||||
internal bool InspectionOnly { get; set; }
|
||||
|
||||
public bool AllowZoom { get; set; }
|
||||
|
||||
public bool AllowSelect { get; set; }
|
||||
@@ -218,6 +222,7 @@ namespace OpenNest.Controls
|
||||
selection.Clear();
|
||||
}
|
||||
|
||||
validationOverlay.Clear();
|
||||
plate = p;
|
||||
plate.PartAdded += plate_PartAdded;
|
||||
plate.PartRemoved += plate_PartRemoved;
|
||||
@@ -231,6 +236,25 @@ namespace OpenNest.Controls
|
||||
}
|
||||
|
||||
internal OverlapOverlayController OverlapOverlay => overlapOverlay;
|
||||
internal ValidationOverlay ValidationOverlay => validationOverlay;
|
||||
internal (float Scale, PointF Origin) CaptureViewport() => (ViewScale, origin);
|
||||
internal void RestoreViewport((float Scale, PointF Origin) viewport)
|
||||
{
|
||||
ViewScale = viewport.Scale;
|
||||
origin = viewport.Origin;
|
||||
UpdateMatrix();
|
||||
Invalidate();
|
||||
}
|
||||
internal void ShowValidationFinding(PostVerificationFinding finding) => validationOverlay.Show(finding);
|
||||
internal void ClearValidationFinding() => validationOverlay.Clear();
|
||||
internal void CenterValidationPoint(Vector location, Point target)
|
||||
{
|
||||
var current = PointWorldToControl(location);
|
||||
origin.X += target.X - current.X;
|
||||
origin.Y += target.Y - current.Y;
|
||||
UpdateMatrix();
|
||||
Invalidate();
|
||||
}
|
||||
public OverlapCheckStatus OverlapStatus => overlapOverlay.Status;
|
||||
public PlateOverlapReport OverlapReport => overlapOverlay.Report;
|
||||
public bool IsOverlapCheckRunning => overlapOverlay.IsRunning;
|
||||
@@ -317,6 +341,8 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnDragEnter(DragEventArgs drgevent)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
return;
|
||||
if (drgevent.Data.GetData(typeof(Drawing)) != null)
|
||||
drgevent.Effect = DragDropEffects.Copy;
|
||||
else if (FilesDropped != null && drgevent.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
@@ -325,6 +351,8 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnDragDrop(DragEventArgs drgevent)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
return;
|
||||
if (drgevent.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
FilesDropped?.Invoke(this, (string[])drgevent.Data.GetData(DataFormats.FileDrop));
|
||||
@@ -351,6 +379,9 @@ namespace OpenNest.Controls
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
middleMouseDownPoint = e.Location;
|
||||
|
||||
if (InspectionOnly)
|
||||
return;
|
||||
|
||||
if (e.Button == MouseButtons.Left && actionManager.CurrentAction is ActionSelect)
|
||||
{
|
||||
var hitCutOff = cutOffHandler.TryStartDrag(CurrentPoint, 5.0 / ViewScale);
|
||||
@@ -373,6 +404,8 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnMouseUp(MouseEventArgs e)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
return;
|
||||
if (e.Button == MouseButtons.Middle && SelectedParts.Count > 0)
|
||||
{
|
||||
var dx = e.X - middleMouseDownPoint.X;
|
||||
@@ -396,11 +429,12 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnMouseWheel(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseWheel(e);
|
||||
if (!InspectionOnly)
|
||||
base.OnMouseWheel(e);
|
||||
|
||||
var multiplier = System.Math.Abs(e.Delta / 120);
|
||||
|
||||
if (SelectedParts.Count > 0 && ((ModifierKeys & Keys.Shift) == Keys.Shift))
|
||||
if (!InspectionOnly && SelectedParts.Count > 0 && ((ModifierKeys & Keys.Shift) == Keys.Shift))
|
||||
{
|
||||
var increment =
|
||||
(ModifierKeys & Keys.Control) == Keys.Control
|
||||
@@ -456,6 +490,9 @@ namespace OpenNest.Controls
|
||||
|
||||
lastPoint = e.Location;
|
||||
|
||||
if (InspectionOnly)
|
||||
return;
|
||||
|
||||
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
||||
{
|
||||
cutOffHandler.UpdateDrag(CurrentPoint, selection.SelectedCutOffs[0]);
|
||||
@@ -484,14 +521,21 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnMouseDoubleClick(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseDoubleClick(e);
|
||||
if (!InspectionOnly)
|
||||
base.OnMouseDoubleClick(e);
|
||||
|
||||
if (e.Button == MouseButtons.Middle && SelectedParts.Count == 0)
|
||||
if (e.Button == MouseButtons.Middle && (InspectionOnly || SelectedParts.Count == 0))
|
||||
ZoomToFit();
|
||||
}
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
{
|
||||
if (keyData == Keys.F)
|
||||
ZoomToFit();
|
||||
return true;
|
||||
}
|
||||
// Audited: DrawControl/PlateView, Action key handlers, EditNestForm,
|
||||
// MainForm.ProcessCmdKey and menu shortcuts have no plain PgUp/PgDn
|
||||
// binding. BestFitViewerForm owns these only in its separate dialog.
|
||||
@@ -504,6 +548,12 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
{
|
||||
if (e.KeyData == Keys.F)
|
||||
ZoomToFit();
|
||||
return;
|
||||
}
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
case Keys.Delete:
|
||||
@@ -529,6 +579,8 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override bool ProcessDialogKey(Keys keyData)
|
||||
{
|
||||
if (InspectionOnly)
|
||||
return true;
|
||||
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
||||
// All other keys can be handled in OnKeyDown method.
|
||||
|
||||
@@ -603,6 +655,7 @@ namespace OpenNest.Controls
|
||||
renderer.DrawActiveWorkArea(e.Graphics);
|
||||
renderer.DrawDebugRemnants(e.Graphics);
|
||||
DrawOverlapOverlay(e.Graphics);
|
||||
validationOverlay.Draw(e.Graphics);
|
||||
|
||||
base.OnPaint(e);
|
||||
|
||||
@@ -633,6 +686,7 @@ namespace OpenNest.Controls
|
||||
{
|
||||
ClearHover();
|
||||
overlapOverlay.ReleaseHandle();
|
||||
validationOverlay.Clear();
|
||||
base.OnHandleDestroyed(e);
|
||||
actionManager.Cleanup();
|
||||
}
|
||||
@@ -649,6 +703,7 @@ namespace OpenNest.Controls
|
||||
plate.PartsReordered -= plate_PartsReordered;
|
||||
}
|
||||
overlapOverlay?.Dispose();
|
||||
validationOverlay?.Dispose();
|
||||
hoverTimer?.Dispose();
|
||||
hoverTimer = null;
|
||||
redrawTimer?.Dispose();
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Controls;
|
||||
|
||||
/// <summary>Display-only findings, valid while the inspector keeps editing disabled.</summary>
|
||||
internal sealed class ValidationOverlay : IDisposable
|
||||
{
|
||||
private readonly PlateView view;
|
||||
private readonly Timer timer;
|
||||
private readonly Stopwatch elapsed = new();
|
||||
private PostVerificationFinding finding;
|
||||
private GraphicsPath areas;
|
||||
private float scale;
|
||||
|
||||
public ValidationOverlay(PlateView view)
|
||||
{
|
||||
this.view = view;
|
||||
timer = new Timer { Interval = 30 };
|
||||
timer.Tick += (_, _) => view.Invalidate();
|
||||
}
|
||||
|
||||
internal bool IsAnimating => timer.Enabled;
|
||||
internal PostVerificationFinding Finding => finding;
|
||||
|
||||
public void Show(PostVerificationFinding value)
|
||||
{
|
||||
Clear();
|
||||
finding = value;
|
||||
var canAnimate = value?.Kind == PostVerificationKind.RapidCrossing
|
||||
? value.RapidStart is { } start && IsFinite(start) && value.RapidEnd is { } end && IsFinite(end)
|
||||
: value?.Location is { } point && IsFinite(point);
|
||||
if (canAnimate)
|
||||
{
|
||||
elapsed.Restart();
|
||||
timer.Start();
|
||||
}
|
||||
view.Invalidate();
|
||||
}
|
||||
|
||||
private static bool IsFinite(Vector point) => double.IsFinite(point.X) && double.IsFinite(point.Y);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
timer.Stop();
|
||||
elapsed.Reset();
|
||||
finding = null;
|
||||
areas?.Dispose();
|
||||
areas = null;
|
||||
view.Invalidate();
|
||||
}
|
||||
|
||||
public void Draw(Graphics graphics)
|
||||
{
|
||||
if (finding == null)
|
||||
return;
|
||||
var state = graphics.Save();
|
||||
try
|
||||
{
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
if (finding.Overlap is { } overlap)
|
||||
{
|
||||
if (areas == null || scale != view.ViewScale)
|
||||
{
|
||||
areas?.Dispose();
|
||||
areas = new GraphicsPath(FillMode.Winding);
|
||||
foreach (var region in overlap.Regions)
|
||||
areas.AddPolygon(region.Vertices.Select(view.PointWorldToGraph).ToArray());
|
||||
scale = view.ViewScale;
|
||||
}
|
||||
using var brush = new SolidBrush(Color.FromArgb(100, 255, 0, 80));
|
||||
graphics.FillPath(brush, areas);
|
||||
}
|
||||
var dpi = view.DeviceDpi / 96f;
|
||||
if (finding.Kind == PostVerificationKind.RapidCrossing)
|
||||
{
|
||||
if (finding.RapidStart is { } start && IsFinite(start)
|
||||
&& finding.RapidEnd is { } end && IsFinite(end))
|
||||
{
|
||||
var pulse = (1 + System.Math.Cos(elapsed.Elapsed.TotalSeconds * System.Math.PI * 2 / 1.2)) / 2;
|
||||
using var rapid = new Pen(Color.FromArgb((int)(65 + 190 * pulse), Color.DarkOrange), 3 * dpi)
|
||||
{ DashStyle = DashStyle.Dash };
|
||||
graphics.DrawLine(rapid, view.PointWorldToGraph(start), view.PointWorldToGraph(end));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (finding.Location is not { } location || !double.IsFinite(location.X) || !double.IsFinite(location.Y))
|
||||
return;
|
||||
var center = view.PointWorldToGraph(location);
|
||||
var phase = timer.Enabled ? (float)(elapsed.Elapsed.TotalSeconds % 0.8 / 0.8) : 0;
|
||||
var radius = (12 + 24 * phase) * dpi;
|
||||
var alpha = (int)(255 * (1 - phase));
|
||||
using var halo = new Pen(Color.FromArgb(alpha, Color.White), 5 * dpi);
|
||||
using var ring = new Pen(Color.FromArgb(alpha, Color.DeepPink), 3 * dpi);
|
||||
graphics.DrawEllipse(halo, center.X - radius, center.Y - radius, radius * 2, radius * 2);
|
||||
graphics.DrawEllipse(ring, center.X - radius, center.Y - radius, radius * 2, radius * 2);
|
||||
graphics.DrawLine(ring, center.X - 5 * dpi, center.Y, center.X + 5 * dpi, center.Y);
|
||||
graphics.DrawLine(ring, center.X, center.Y - 5 * dpi, center.X, center.Y + 5 * dpi);
|
||||
}
|
||||
finally
|
||||
{
|
||||
graphics.Restore(state);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
timer.Dispose();
|
||||
areas?.Dispose();
|
||||
}
|
||||
}
|
||||
Generated
+11
-2
@@ -1,4 +1,4 @@
|
||||
namespace OpenNest.Forms
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
@@ -95,6 +95,7 @@
|
||||
runAutoNestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
autoSequenceAllPlatesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestRemoveEmptyPlates = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestValidate = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestPost = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem19 = new System.Windows.Forms.ToolStripSeparator();
|
||||
calculateCutTimeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -571,7 +572,7 @@
|
||||
//
|
||||
// mnuNest
|
||||
//
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||
mnuNest.Name = "mnuNest";
|
||||
mnuNest.Size = new System.Drawing.Size(43, 20);
|
||||
mnuNest.Text = "&Nest";
|
||||
@@ -668,6 +669,13 @@
|
||||
mnuNestRemoveEmptyPlates.Text = "Remove Empty Plates";
|
||||
mnuNestRemoveEmptyPlates.Click += RemoveEmptyPlates_Click;
|
||||
//
|
||||
// mnuNestValidate
|
||||
//
|
||||
mnuNestValidate.Name = "mnuNestValidate";
|
||||
mnuNestValidate.Size = new System.Drawing.Size(205, 22);
|
||||
mnuNestValidate.Text = "Validate Nest...";
|
||||
mnuNestValidate.Click += ValidateNest_Click;
|
||||
//
|
||||
// mnuNestPost
|
||||
//
|
||||
mnuNestPost.Name = "mnuNestPost";
|
||||
@@ -1420,6 +1428,7 @@
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem23;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem24;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem12;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestValidate;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestPost;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem13;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlateViewInCad;
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace OpenNest.Forms
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private EditNestForm activeForm;
|
||||
private ValidateNestForm validationForm;
|
||||
private bool clickUpdateLocation;
|
||||
private bool nestingInProgress;
|
||||
private CancellationTokenSource nestingCts;
|
||||
@@ -1553,6 +1554,34 @@ namespace OpenNest.Forms
|
||||
activeForm.AutoSequenceAllPlates();
|
||||
}
|
||||
|
||||
private void ValidateNest_Click(object sender, EventArgs e)
|
||||
{
|
||||
var editForm = activeForm;
|
||||
if (editForm == null)
|
||||
return;
|
||||
|
||||
var views = MdiChildren.OfType<EditNestForm>()
|
||||
.Where(form => ReferenceEquals(form.Nest, editForm.Nest))
|
||||
.Select(form => form.PlateView).ToArray();
|
||||
if (nestingInProgress || Application.OpenForms.OfType<NestProgressForm>().Any()
|
||||
|| views.Any(view => view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true))
|
||||
{
|
||||
MessageBox.Show(this, "Finish or cancel the current nesting or plate action before validating.",
|
||||
"Validate Nest", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var view in views)
|
||||
view.SetAction(typeof(ActionSelect));
|
||||
if (validationForm is { IsDisposed: false })
|
||||
{
|
||||
validationForm.Activate();
|
||||
return;
|
||||
}
|
||||
validationForm = new ValidateNestForm(editForm);
|
||||
validationForm.Show(this);
|
||||
}
|
||||
|
||||
private void PostProcessor_Click(object sender, EventArgs e)
|
||||
{
|
||||
var menuItem = (ToolStripMenuItem)sender;
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Diagnostics;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
/// <summary>Modeless inspection; the editor scope blocks edits while leaving canvas navigation available.</summary>
|
||||
public sealed class ValidateNestForm : Form
|
||||
{
|
||||
private readonly Nest nest;
|
||||
private readonly ValidationInspectionScope inspection;
|
||||
private readonly Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze;
|
||||
private readonly CancellationTokenSource cancellation = new();
|
||||
private readonly Label summaryLabel;
|
||||
private readonly TextBox reportBox;
|
||||
private readonly DataGridView warningGrid;
|
||||
private readonly EditNestForm editor;
|
||||
private readonly int originalPlate;
|
||||
private bool populating;
|
||||
private bool editorRestored;
|
||||
private readonly (float Scale, PointF Origin) originalViewport;
|
||||
private readonly OverlapDisplayMode originalOverlapDisplay;
|
||||
private readonly Button closeButton;
|
||||
private bool started;
|
||||
private bool analyzing;
|
||||
private bool closeRequested;
|
||||
private bool disposedResources;
|
||||
|
||||
public ValidateNestForm(Nest nest)
|
||||
: this(nest, (value, token) => Task.Run(() => PostVerificationAnalyzer.Analyze(value, token), token))
|
||||
{ }
|
||||
|
||||
public ValidateNestForm(EditNestForm editor)
|
||||
: this(editor, (value, token) => Task.Run(() => PostVerificationAnalyzer.Analyze(value, token), token))
|
||||
{ }
|
||||
|
||||
internal ValidateNestForm(EditNestForm editor, Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze)
|
||||
: this(editor.Nest, analyze)
|
||||
{
|
||||
this.editor = editor;
|
||||
inspection = new ValidationInspectionScope(editor, this);
|
||||
originalPlate = editor.PlateManager.CurrentIndex;
|
||||
originalViewport = editor.PlateView.CaptureViewport();
|
||||
originalOverlapDisplay = editor.PlateView.OverlapDisplay;
|
||||
editor.PlateView.OverlapDisplay = OverlapDisplayMode.Off;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
MinimumSize = new Size(480, 440);
|
||||
ClientSize = new Size(560, 650);
|
||||
}
|
||||
|
||||
internal ValidateNestForm(Nest nest, Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze)
|
||||
{
|
||||
this.nest = nest ?? throw new ArgumentNullException(nameof(nest));
|
||||
this.analyze = analyze ?? throw new ArgumentNullException(nameof(analyze));
|
||||
Text = "Validate Nest";
|
||||
Name = nameof(ValidateNestForm);
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
ClientSize = new Size(850, 560);
|
||||
MinimumSize = new Size(700, 440);
|
||||
ShowInTaskbar = true;
|
||||
MinimizeBox = true;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = 4,
|
||||
};
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
summaryLabel = new Label
|
||||
{
|
||||
Name = "summaryLabel",
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
Text = "Checking all plates: overlaps, missing lead-ins, and rapid crossings...",
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
};
|
||||
var scopeLabel = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
Text = "Select an issue to locate it in the main plate view. Its highlight pulses while selected.\r\n"
|
||||
+ "Minimum spacing, plate boundaries, and final post-processor output are not checked.",
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
};
|
||||
reportBox = new TextBox
|
||||
{
|
||||
Name = "reportBox",
|
||||
Dock = DockStyle.Fill,
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
WordWrap = true,
|
||||
MaxLength = 0,
|
||||
AccessibleName = "Nest validation findings",
|
||||
Text = "Validation is running...",
|
||||
};
|
||||
warningGrid = new DataGridView
|
||||
{
|
||||
Name = "warningGrid",
|
||||
Dock = DockStyle.Fill,
|
||||
ReadOnly = true,
|
||||
AllowUserToAddRows = false,
|
||||
AllowUserToDeleteRows = false,
|
||||
MultiSelect = false,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||
AutoGenerateColumns = false,
|
||||
RowHeadersVisible = false,
|
||||
BackgroundColor = SystemColors.Window,
|
||||
AccessibleName = "Nest validation findings",
|
||||
};
|
||||
foreach (var title in new[] { "Plate", "Part", "With", "Issue", "Finding" })
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = title,
|
||||
Width = title == "Issue" ? 115 : 48,
|
||||
AutoSizeMode = title == "Finding" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable,
|
||||
});
|
||||
warningGrid.CurrentCellChanged += (_, _) => ShowSelectedFinding();
|
||||
warningGrid.CellClick += (_, _) => ShowSelectedFinding();
|
||||
var findings = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1 };
|
||||
findings.RowStyles.Add(new RowStyle(SizeType.Percent, 65));
|
||||
findings.RowStyles.Add(new RowStyle(SizeType.Percent, 35));
|
||||
findings.Controls.Add(warningGrid, 0, 0);
|
||||
findings.Controls.Add(reportBox, 0, 1);
|
||||
closeButton = new Button
|
||||
{
|
||||
Name = "closeButton",
|
||||
Text = "Cancel",
|
||||
AutoSize = true,
|
||||
DialogResult = DialogResult.Cancel,
|
||||
};
|
||||
closeButton.Click += (_, _) => Close();
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoSize = true,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
};
|
||||
buttons.Controls.Add(closeButton);
|
||||
layout.Controls.Add(summaryLabel, 0, 0);
|
||||
layout.Controls.Add(scopeLabel, 0, 1);
|
||||
layout.Controls.Add(findings, 0, 2);
|
||||
layout.Controls.Add(buttons, 0, 3);
|
||||
Controls.Add(layout);
|
||||
AcceptButton = closeButton;
|
||||
CancelButton = closeButton;
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
if (editor == null)
|
||||
return;
|
||||
var area = Screen.FromControl(editor).WorkingArea;
|
||||
Bounds = new Rectangle(area.Right - Width, area.Top, Width, System.Math.Min(Height, area.Height));
|
||||
}
|
||||
|
||||
private void ShowSelectedFinding()
|
||||
{
|
||||
if (editorRestored || populating || editor == null || editor.IsDisposed)
|
||||
return;
|
||||
var view = editor.PlateView;
|
||||
view.ClearValidationFinding();
|
||||
if (warningGrid.CurrentRow?.Tag is not PostVerificationFinding finding
|
||||
|| finding.PlateNumber < 1 || finding.PlateNumber > nest.Plates.Count)
|
||||
return;
|
||||
if (editor.PlateManager.CurrentIndex != finding.PlateNumber - 1)
|
||||
editor.PlateManager.LoadAt(finding.PlateNumber - 1);
|
||||
var location = finding.Location;
|
||||
if (location == null && finding.PartNumber is { } part && part > 0 && part <= view.Plate.Parts.Count)
|
||||
location = view.Plate.Parts[part - 1]?.BoundingBox?.Center;
|
||||
if (location is { } point && double.IsFinite(point.X) && double.IsFinite(point.Y))
|
||||
{
|
||||
var extent = finding.Overlap?.Bounds;
|
||||
var size = System.Math.Max(10, System.Math.Max(extent?.Length ?? 0, extent?.Width ?? 0) * 2);
|
||||
if (finding.RapidStart is { } start && finding.RapidEnd is { } end)
|
||||
size = System.Math.Max(size, start.DistanceTo(end) * 1.5);
|
||||
view.ZoomToArea(point.X - size / 2, point.Y - size / 2, size, size);
|
||||
// Keep the marker in the portion of the main canvas left of this floating inspector.
|
||||
var screen = view.RectangleToScreen(view.ClientRectangle);
|
||||
var available = System.Math.Clamp(Left - screen.Left, 0, view.Width);
|
||||
if (available > 100)
|
||||
view.CenterValidationPoint(point, new Point(available / 2, view.Height / 2));
|
||||
}
|
||||
else
|
||||
view.ZoomToFit();
|
||||
view.ShowValidationFinding(finding with { Location = location });
|
||||
ShowFindingDetails(finding);
|
||||
}
|
||||
|
||||
private void ShowFindingDetails(PostVerificationFinding finding)
|
||||
{
|
||||
var hint = finding.Kind switch
|
||||
{
|
||||
PostVerificationKind.Overlap => "Shaded material is the overlap. The ring marks its area centroid, which can lie between separate patches.",
|
||||
PostVerificationKind.RapidCrossing => "The pulsing dashed orange line is the offending rapid segment; it does not mark an exact contact point.",
|
||||
PostVerificationKind.MissingLeadIn => "The ring marks the cutting contour start where a lead-in is missing.",
|
||||
_ => "The ring marks the affected part's center when available; no exact problem location is known.",
|
||||
};
|
||||
reportBox.Text = $"Plate {finding.PlateNumber}, part {finding.PartNumber}: {finding.Message}"
|
||||
+ Environment.NewLine + Environment.NewLine + hint;
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
RestoreEditor();
|
||||
base.OnFormClosed(e);
|
||||
}
|
||||
|
||||
private void RestoreEditor()
|
||||
{
|
||||
if (editorRestored || editor == null || editor.IsDisposed)
|
||||
return;
|
||||
editorRestored = true;
|
||||
inspection?.Dispose();
|
||||
editor.PlateView.ClearValidationFinding();
|
||||
if (editor.PlateManager.CurrentIndex != originalPlate)
|
||||
editor.PlateManager.LoadAt(originalPlate);
|
||||
editor.PlateView.RestoreViewport(originalViewport);
|
||||
editor.PlateView.OverlapDisplay = originalOverlapDisplay;
|
||||
}
|
||||
|
||||
protected override async void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
await ValidateAsync();
|
||||
}
|
||||
|
||||
internal async Task ValidateAsync()
|
||||
{
|
||||
if (started)
|
||||
return;
|
||||
started = true;
|
||||
analyzing = true;
|
||||
try
|
||||
{
|
||||
var report = await analyze(nest, cancellation.Token);
|
||||
if (closeRequested || IsDisposed)
|
||||
return;
|
||||
populating = true;
|
||||
foreach (var finding in report.Findings)
|
||||
{
|
||||
var index = warningGrid.Rows.Add(finding.PlateNumber, finding.PartNumber, finding.OtherPartNumber,
|
||||
finding.Kind, finding.Message);
|
||||
warningGrid.Rows[index].Tag = finding;
|
||||
}
|
||||
populating = false;
|
||||
ShowSelectedFinding();
|
||||
reportBox.Text = report.ToDisplayText("Nest validation");
|
||||
if (editor != null && warningGrid.CurrentRow?.Tag is PostVerificationFinding selected)
|
||||
ShowFindingDetails(selected);
|
||||
reportBox.SelectionStart = 0;
|
||||
reportBox.SelectionLength = 0;
|
||||
summaryLabel.Text = report.Findings.Any(finding => finding.Kind == PostVerificationKind.Incomplete)
|
||||
? "Validation incomplete. Review the findings; this nest has not passed all checks."
|
||||
: report.HasWarnings ? "Warnings found. Review the findings below."
|
||||
: "Validation complete. No warnings found by these checks.";
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
closeRequested = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
summaryLabel.Text = "Validation failed. This nest has not passed the checks.";
|
||||
reportBox.Text = "Validation could not finish. Close this window and try again."
|
||||
+ Environment.NewLine + Environment.NewLine + ex.Message;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
analyzing = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
closeButton.Text = "Close";
|
||||
if (closeRequested)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (analyzing)
|
||||
{
|
||||
closeRequested = true;
|
||||
cancellation.Cancel();
|
||||
closeButton.Enabled = false;
|
||||
summaryLabel.Text = "Canceling validation...";
|
||||
e.Cancel = true;
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !disposedResources)
|
||||
{
|
||||
disposedResources = true;
|
||||
RestoreEditor();
|
||||
inspection?.Dispose();
|
||||
cancellation.Cancel();
|
||||
cancellation.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
/// <summary>Keep live validation inputs stable while allowing navigation of the main canvas.</summary>
|
||||
internal sealed class ValidationInspectionScope : IDisposable
|
||||
{
|
||||
private readonly List<(Control Control, bool Enabled)> controls = new();
|
||||
private readonly List<(Form Form, bool AllowDrop)> hosts = new();
|
||||
private bool disposed;
|
||||
private readonly PlateView view;
|
||||
private readonly Form inspector;
|
||||
private readonly bool previousInspection;
|
||||
|
||||
public ValidationInspectionScope(EditNestForm editor, Form inspector)
|
||||
{
|
||||
view = editor.PlateView;
|
||||
this.inspector = inspector;
|
||||
previousInspection = view.InspectionOnly;
|
||||
view.InspectionOnly = true;
|
||||
var root = editor.MdiParent ?? editor;
|
||||
DisableExceptCanvas(root);
|
||||
hosts.Add((root, root.AllowDrop));
|
||||
if (!ReferenceEquals(root, editor))
|
||||
hosts.Add((editor, editor.AllowDrop));
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
host.Form.AllowDrop = false;
|
||||
host.Form.FormClosing += HostClosing;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableExceptCanvas(Control parent)
|
||||
{
|
||||
foreach (var child in parent.Controls.Cast<Control>())
|
||||
{
|
||||
if (ReferenceEquals(child, view))
|
||||
continue;
|
||||
if (child.Contains(view))
|
||||
DisableExceptCanvas(child);
|
||||
else
|
||||
{
|
||||
controls.Add((child, child.Enabled));
|
||||
child.Enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HostClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
// A worker may still read the nest. Finish closing the inspector first.
|
||||
e.Cancel = true;
|
||||
inspector.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
disposed = true;
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
host.Form.FormClosing -= HostClosing;
|
||||
if (!host.Form.IsDisposed)
|
||||
host.Form.AllowDrop = host.AllowDrop;
|
||||
}
|
||||
foreach (var saved in controls.Where(saved => !saved.Control.IsDisposed))
|
||||
saved.Control.Enabled = saved.Enabled;
|
||||
if (!view.IsDisposed)
|
||||
view.InspectionOnly = previousInspection;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,41 @@
|
||||
# Verify a nest before posting
|
||||
|
||||
## Standalone desktop validation
|
||||
|
||||
Choose **Nest > Validate Nest...** to inspect all plates without running the cut
|
||||
planner or selecting a post processor. The read-only **Validate Nest** window
|
||||
checks overlapping material, missing lead-ins, and rapid crossings in the existing
|
||||
placed programs and cutting order. Findings identify plate and part sequence
|
||||
numbers in a read-only grid like the cut planner; incomplete or failed checks are
|
||||
never reported as clear. Select a row to switch the main plate view to that plate
|
||||
and focus the finding. The inspector opens at the right of the screen and can be
|
||||
moved or resized. The details below the grid explain the selected warning.
|
||||
|
||||
Overlaps use the same magenta material shading as **View > Overlap Check**. A
|
||||
DPI-scaled ring expands and fades continuously while the finding is selected;
|
||||
both its color and contrasting halo fade as it grows. For overlaps the ring marks the area centroid (possibly between
|
||||
separate patches); for missing lead-ins it marks the contour start. Rapid warnings
|
||||
pulse the offending orange dashed segment continuously, without a ring or an
|
||||
exact contact-point marker. Incomplete findings use the part center when available;
|
||||
findings without a usable location do not invent a marker. Closing the inspector
|
||||
clears the highlight and restores the original plate, zoom, and overlap display mode.
|
||||
|
||||
Validation does not change placements, programs, lead-ins, or cutting order, save
|
||||
the nest, or write CNC output. Minimum spacing and plate boundaries are not checked.
|
||||
It does not verify a post processor's final output. Posting still runs its own
|
||||
fresh verification and consent flow.
|
||||
|
||||
Finish or cancel active nesting/fill operations before validating. The window is
|
||||
a normal top-level, modeless form. Editing controls and shortcuts stay disabled
|
||||
while it is open, but the main plate view remains available for wheel zoom,
|
||||
middle-button pan, and **F** or middle-button double-click to fit. Close the
|
||||
inspector to resume editing. Cancel, Escape, or closing the window requests
|
||||
cancellation and waits for the worker to stop before restoring editing controls.
|
||||
Closing the main window during inspection closes/cancels the inspector first;
|
||||
close the main window again after inspection has finished.
|
||||
|
||||
## Posting workflow
|
||||
|
||||
The desktop's **Nest > Post > (post processor)** workflow checks every plate after
|
||||
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:
|
||||
|
||||
Reference in new issue
Block a user