mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 22:45:45 -05:00
The Layout label counted parts off the plate or inside the edge spacing, but nothing showed which part or where, so the user had to hunt for it. Each edge finding now gets a canvas marker like the overlap and spacing ones: a red crosshair where a part leaves the sheet and an orange ring at the gap to the edge, in every visible display mode, with hover text that carries the finding's message. Markers are retained across edits like the other overlay findings (only a changed part's marker drops), and a sheet size, edge spacing or outline change clears them. Validate Nest already located these findings through PostVerificationFinding.Location. "Near edge" / "inside the edge spacing" read as a spacing rule rather than a placement problem, so the label, overlay messages, PlateEdgeReview findings and the Validate Nest category now say the part is outside the work area. Off the plate keeps its wording. The PostVerificationKind.EdgeSpacing name stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1425 lines
60 KiB
C#
1425 lines
60 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Windows.Forms;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Controls;
|
|
using OpenNest.Diagnostics;
|
|
using OpenNest.Forms;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.WinForms.Tests;
|
|
|
|
[CollectionDefinition("Overlap overlay", DisableParallelization = true)]
|
|
public class OverlapOverlayCollection;
|
|
|
|
[Collection("Overlap overlay")]
|
|
public class PlateOverlapOverlayTests
|
|
{
|
|
[Fact]
|
|
public void RestartAndCancelRejectLateCompletionsOnTheUiThread() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var uiThread = Environment.CurrentManagedThreadId;
|
|
run.View.OverlapStateChanged += (_, _) => Assert.Equal(uiThread, Environment.CurrentManagedThreadId);
|
|
var first = run.Start();
|
|
var old = run.Next();
|
|
var second = run.Start();
|
|
var current = run.Next();
|
|
Assert.True(old.Token.IsCancellationRequested);
|
|
old.Complete();
|
|
run.Pump(first);
|
|
Assert.Equal(OverlapCheckStatus.Checking, run.View.OverlapStatus);
|
|
current.Complete();
|
|
run.Pump(second);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.Single(run.View.OverlapReport.Pairs);
|
|
|
|
var canceled = run.Start();
|
|
var late = run.Next();
|
|
run.View.CancelOverlapCheck();
|
|
Assert.True(late.Token.IsCancellationRequested);
|
|
late.Complete();
|
|
run.Pump(canceled);
|
|
Assert.Equal(OverlapCheckStatus.Canceled, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("move")]
|
|
[InlineData("add")]
|
|
[InlineData("remove")]
|
|
[InlineData("replace")]
|
|
[InlineData("reorder")]
|
|
[InlineData("editor")]
|
|
[InlineData("plate")]
|
|
public void EditOrPlateSwitchRejectsPendingReport(string edit) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var task = run.Start();
|
|
var work = run.Next();
|
|
var plate = run.View.Plate;
|
|
switch (edit)
|
|
{
|
|
case "move": plate.Parts[0].Offset(1e-10, 0); break;
|
|
case "add": plate.Parts.Add(Rectangle()); break;
|
|
case "remove": plate.Parts.RemoveAt(0); break;
|
|
case "replace": plate.Parts[0] = Rectangle(); break;
|
|
case "reorder": (plate.Parts[0], plate.Parts[1]) = (plate.Parts[1], plate.Parts[0]); break;
|
|
case "editor": run.View.InvalidateOverlapCheck(); break;
|
|
case "plate": run.View.Plate = new Plate(); break;
|
|
}
|
|
if (edit != "move")
|
|
Assert.True(work.Token.IsCancellationRequested);
|
|
work.Complete();
|
|
run.Pump(task);
|
|
Assert.True(work.Token.IsCancellationRequested);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.Equal(edit == "plate" ? OverlapCheckStatus.NotChecked : OverlapCheckStatus.Stale,
|
|
run.View.OverlapStatus);
|
|
});
|
|
|
|
[Fact]
|
|
public void PaintChecksFreshnessEvenWhenDisplayIsOff() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.Finish();
|
|
using var image = new Bitmap(240, 240);
|
|
using var graphics = Graphics.FromImage(image);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
});
|
|
|
|
[Fact]
|
|
public void UnchangedPairStaysVisibleDuringMotionAndPendingRecheck() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Add(Rectangle(20));
|
|
run.View.Plate.Parts.Add(Rectangle(21));
|
|
run.Finish();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
using var image = new Bitmap(240, 240);
|
|
using var graphics = Graphics.FromImage(image);
|
|
var overlay = run.View.OverlapOverlay;
|
|
var untouchedPoint = run.View.PointWorldToGraph(new Vector(2, 2));
|
|
var movedPoint = run.View.PointWorldToGraph(new Vector(22, 2));
|
|
overlay.Draw(graphics);
|
|
Assert.True(overlay.CachedPath.IsVisible(untouchedPoint));
|
|
Assert.True(overlay.CachedPath.IsVisible(movedPoint));
|
|
|
|
run.View.Plate.Parts[3].Offset(1e-10, 0);
|
|
overlay.Draw(graphics);
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.True(overlay.CachedPath.IsVisible(untouchedPoint));
|
|
Assert.False(overlay.CachedPath.IsVisible(movedPoint));
|
|
var retainedPath = overlay.CachedPath;
|
|
run.View.Plate.Parts[3].Offset(1, 0);
|
|
overlay.Draw(graphics);
|
|
Assert.Same(retainedPath, overlay.CachedPath);
|
|
Assert.Equal(1, run.Calls); // Paint never analyzes.
|
|
|
|
var task = run.Start();
|
|
var pending = run.Next();
|
|
overlay.Draw(graphics); // Both areas and centroid markers must render without a full report.
|
|
Assert.Equal(OverlapCheckStatus.Checking, run.View.OverlapStatus);
|
|
Assert.Same(retainedPath, overlay.CachedPath);
|
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
|
overlay.Draw(graphics);
|
|
Assert.Null(overlay.CachedPath);
|
|
pending.Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
overlay.Draw(graphics);
|
|
Assert.Null(overlay.CachedPath); // Late completion cannot restore the removed pair.
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("editor")]
|
|
[InlineData("cancel")]
|
|
[InlineData("failure")]
|
|
public void RetainedPathClearsOnHardInvalidationOrRequestFailure(string edit) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Add(Rectangle(20));
|
|
run.Finish();
|
|
run.View.Plate.Parts[2].Offset(1, 0);
|
|
run.Paint();
|
|
var path = run.View.OverlapOverlay.CachedPath;
|
|
Assert.NotNull(path);
|
|
var task = run.Start();
|
|
var pending = run.Next();
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
switch (edit)
|
|
{
|
|
case "editor": run.View.InvalidateOverlapCheck(); pending.Complete(); break;
|
|
case "cancel": run.View.CancelOverlapCheck(); pending.Complete(); break;
|
|
case "failure": pending.Fail(); break;
|
|
}
|
|
run.Pump(task);
|
|
Assert.Null(run.View.OverlapReport);
|
|
run.Paint();
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
});
|
|
|
|
[Fact]
|
|
public void DisplayPanZoomAndPreviewDoNotAnalyzeOrChangeCommittedParts() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var parts = run.View.Plate.Parts.ToArray();
|
|
var locations = parts.Select(p => p.Location).ToArray();
|
|
var programs = parts.Select(p => p.Program).ToArray();
|
|
var quantities = parts.Select(p => p.BaseDrawing.Quantity.Nested).ToArray();
|
|
run.View.SelectAll();
|
|
var selection = run.View.SelectedParts.ToArray();
|
|
run.View.SetActiveParts(new List<Part> { Rectangle() });
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
|
run.Finish();
|
|
Assert.Equal(OverlapDisplayMode.Areas, run.View.OverlapDisplay);
|
|
Assert.Single(run.View.OverlapReport.Pairs); // Preview is not a third input.
|
|
var report = run.View.OverlapReport;
|
|
using var image = new Bitmap(240, 240);
|
|
using var graphics = Graphics.FromImage(image);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
var path = run.View.OverlapOverlay.CachedPath;
|
|
Assert.Equal(FillMode.Winding, path.FillMode);
|
|
run.View.Pan(35, -20);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Areas;
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
run.View.ZoomToPoint(new Vector(), 2);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.NotSame(path, run.View.OverlapOverlay.CachedPath);
|
|
Assert.Same(report, run.View.OverlapReport);
|
|
Assert.Equal(1, run.Calls);
|
|
Assert.Equal(parts, run.View.Plate.Parts.ToArray());
|
|
Assert.Equal(locations, parts.Select(p => p.Location));
|
|
Assert.Equal(programs, parts.Select(p => p.Program));
|
|
Assert.Equal(quantities, parts.Select(p => p.BaseDrawing.Quantity.Nested));
|
|
Assert.Equal(selection, run.View.SelectedParts);
|
|
});
|
|
|
|
[Fact]
|
|
public void TripleOverlapUsesOneFillAndGraphCoordinatesDoNotDoublePan() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Clear();
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.View.Plate.Parts.Add(Rectangle(2));
|
|
run.View.Plate.Parts.Add(Rectangle(3));
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.Clear(Color.White);
|
|
graphics.TranslateTransform(60, 220);
|
|
var transform = graphics.Transform.Elements;
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Equal(transform, graphics.Transform.Elements); // Label restores graph space.
|
|
Assert.Equal(image.GetPixel(110, 180), image.GetPixel(130, 180)); // Double/triple material, same alpha.
|
|
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(130, 180).ToArgb());
|
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(70, 180).ToArgb());
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void HandleDestructionOrDisposalNeverResurrectsRequests(bool dispose) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.Finish();
|
|
using var image = new Bitmap(100, 100);
|
|
using var graphics = Graphics.FromImage(image);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
|
var task = run.Start();
|
|
var work = run.Next();
|
|
if (dispose)
|
|
run.View.Dispose();
|
|
else
|
|
run.View.Recreate();
|
|
Assert.True(work.Token.IsCancellationRequested);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
work.Complete();
|
|
run.Pump(task);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.False(run.View.IsOverlapCheckRunning);
|
|
});
|
|
|
|
[Fact]
|
|
public void CompletedPathIsReleasedOnHandleRecreationAndCannotReappear() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.Finish();
|
|
using var image = new Bitmap(100, 100);
|
|
using var graphics = Graphics.FromImage(image);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
|
run.View.Recreate();
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void FailureAndIncompleteAreVisibleAndNeverReportedAsClear() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var task = run.Start();
|
|
run.Next().Fail();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Failed, run.View.OverlapStatus);
|
|
Assert.Contains("failed", run.View.Status);
|
|
Assert.Null(run.View.OverlapReport);
|
|
run.View.Plate.Parts.Add(new Part(new Drawing("invalid", new Program())));
|
|
run.Finish();
|
|
Assert.Equal(OverlapCheckStatus.Incomplete, run.View.OverlapStatus);
|
|
Assert.Contains("incomplete: 1 overlapping pairs; 1 parts", run.View.Status);
|
|
});
|
|
|
|
[Fact]
|
|
public void LabelReportsPartSpacingAndRechecksWhenItChanges() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.PartSpacing = 0.25;
|
|
run.View.Plate.Parts.Add(Rectangle(5.1));
|
|
run.Finish();
|
|
Assert.Equal("Overlaps: 1 pairs; part spacing: 1 pairs too close", run.View.OverlapOverlay.Message);
|
|
Assert.Equal(run.View.OverlapOverlay.Message, run.View.Status);
|
|
|
|
run.View.Plate.PartSpacing = 0.05;
|
|
run.Paint();
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
run.Finish();
|
|
Assert.Equal("Overlaps: 1 pairs", run.View.OverlapOverlay.Message);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData(OverlapDisplayMode.Areas, true)]
|
|
[InlineData(OverlapDisplayMode.Centroids, true)]
|
|
[InlineData(OverlapDisplayMode.Both, true)]
|
|
[InlineData(OverlapDisplayMode.Off, false)]
|
|
public void SpacingRingDrawsAndHoversInEveryVisibleMode(OverlapDisplayMode mode, bool visible) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Clear();
|
|
run.View.Plate.PartSpacing = 0.25;
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.View.Plate.Parts.Add(Rectangle(4.1));
|
|
run.Finish();
|
|
run.View.OverlapDisplay = mode;
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.Clear(Color.White);
|
|
graphics.TranslateTransform(60, 220);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
|
|
var pair = Assert.Single(run.View.OverlapReport.SpacingPairs);
|
|
var center = run.View.PointWorldToControl(pair.Marker);
|
|
var radius = (int)OverlapPairPresentation.MarkerHalfSize(run.View.DeviceDpi);
|
|
Assert.Equal(visible, image.GetPixel(center.X - radius, center.Y).ToArgb() != Color.White.ToArgb());
|
|
run.View.MoveTo(center);
|
|
if (visible)
|
|
Assert.Contains("Part spacing 1/2", run.View.OverlapOverlay.HoverText);
|
|
else
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
});
|
|
|
|
[Fact]
|
|
public void EdgeMarkersDrawAndHoverWithTheFindingsMessage() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Clear();
|
|
run.View.Plate.EdgeSpacing = new Spacing(0.5, 0, 0, 0);
|
|
run.View.Plate.Parts.Add(Rectangle(0.2));
|
|
var hanging = Rectangle(-1);
|
|
hanging.Offset(0, 5);
|
|
run.View.Plate.Parts.Add(hanging);
|
|
run.Finish();
|
|
Assert.Equal("Layout: 1 off plate, 1 outside work area", run.View.OverlapSummary);
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.Clear(Color.White);
|
|
graphics.TranslateTransform(60, 220);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
|
|
var radius = (int)OverlapPairPresentation.MarkerHalfSize(run.View.DeviceDpi);
|
|
foreach (var finding in run.View.OverlapReport.EdgeFindings)
|
|
{
|
|
var center = run.View.PointWorldToControl(finding.Marker);
|
|
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(center.X - radius, center.Y).ToArgb());
|
|
run.View.MoveTo(center);
|
|
Assert.Contains(finding.Message, run.View.OverlapOverlay.HoverText);
|
|
Assert.StartsWith(finding.OffPlate ? "Off the plate: part 2" : "Outside work area: part 1",
|
|
run.View.OverlapOverlay.HoverText);
|
|
}
|
|
});
|
|
|
|
[Fact]
|
|
public void ClearPlateDrawsNoLabelButStatusKeepsTheWords() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.RemoveAt(1);
|
|
run.Finish();
|
|
Assert.True(run.View.OverlapOverlay.ShowsNoLabel);
|
|
Assert.Equal("No material overlaps detected", run.View.Status);
|
|
Assert.True(CornerIsEmpty(run));
|
|
|
|
run.View.Plate.Parts.Add(Rectangle(1));
|
|
run.Finish();
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel);
|
|
Assert.False(CornerIsEmpty(run));
|
|
});
|
|
|
|
[Fact]
|
|
public void ClearPlateStaysEmptyWhileRechecking() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.RemoveAt(1);
|
|
run.EnableAutoCheck();
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.ShowsNoLabel); // Before the first result too.
|
|
var task = run.AutoTick();
|
|
var work = run.Next();
|
|
Assert.True(overlay.ShowsNoLabel);
|
|
work.Complete();
|
|
run.Pump(task);
|
|
Assert.True(overlay.ShowsNoLabel);
|
|
|
|
run.View.Plate.Parts[0].Offset(1, 0);
|
|
run.Paint();
|
|
Assert.True(overlay.IsAutoCheckScheduled);
|
|
Assert.True(overlay.ShowsNoLabel);
|
|
Assert.True(CornerIsEmpty(run));
|
|
});
|
|
|
|
[Fact]
|
|
public void FindingsStayInTheLabelWhileRechecking() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.EnableAutoCheck();
|
|
var task = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
|
|
run.View.Plate.Parts[1].Offset(0.5, 0);
|
|
run.Paint();
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("failed")]
|
|
[InlineData("canceled")]
|
|
public void FailedOrCanceledCheckIsNeverAnEmptyCorner(string outcome) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.RemoveAt(1);
|
|
run.EnableAutoCheck();
|
|
var task = run.AutoTick();
|
|
var work = run.Next();
|
|
if (outcome == "failed")
|
|
work.Fail();
|
|
else
|
|
{
|
|
run.View.CancelOverlapCheck();
|
|
work.Complete();
|
|
}
|
|
run.Pump(task);
|
|
run.Paint();
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel);
|
|
});
|
|
|
|
[Fact]
|
|
public void WithoutAutoCheckPendingStatesKeepTheirText() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.RemoveAt(1);
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel); // Not checked.
|
|
run.Finish();
|
|
run.View.Plate.Parts[0].Offset(1, 0);
|
|
run.Paint();
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel);
|
|
var task = run.Start();
|
|
var work = run.Next();
|
|
Assert.False(run.View.OverlapOverlay.ShowsNoLabel); // Checking.
|
|
work.Complete();
|
|
run.Pump(task);
|
|
});
|
|
|
|
[Fact]
|
|
public void SummaryStaysSteadyThroughARecheckAndNotifiesFromPaint() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.RemoveAt(1);
|
|
run.EnableAutoCheck();
|
|
var overlay = run.View.OverlapOverlay;
|
|
var task = run.AutoTick();
|
|
Assert.Equal("Layout: checking…", overlay.Summary);
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal("Layout: OK", overlay.Summary);
|
|
|
|
var changes = new List<string>();
|
|
run.View.OverlapStateChanged += (_, _) => changes.Add(overlay.Summary);
|
|
run.View.Plate.Parts[0].Offset(1, 0); // A drag: only paint can notice it.
|
|
run.Paint();
|
|
Assert.Equal("Layout: checking…", Assert.Single(changes.Distinct()));
|
|
Assert.False(overlay.NeedsAttention);
|
|
});
|
|
|
|
[Fact]
|
|
public void StatusBarLabelFollowsTheActiveViewWithoutTouchingTheStatusText() => RunSta(() =>
|
|
{
|
|
using var form = new EditNestForm(new Nest("layout"));
|
|
var status = new ToolStripStatusLabel();
|
|
var layout = MainStatusBar.CreateLayoutLabel();
|
|
var bar = new MainStatusBar(status, new ToolStripStatusLabel(), new ToolStripStatusLabel(),
|
|
new ToolStripStatusLabel(), new ToolStripStatusLabel(), new ToolStripStatusLabel(), null, layout);
|
|
form.PlateView.Status = "Fill: 2 parts in 5 ms";
|
|
|
|
bar.Bind(form);
|
|
Assert.True(layout.Available);
|
|
Assert.Equal(form.PlateView.OverlapSummary, layout.Text);
|
|
Assert.Equal(form.PlateView.OverlapMessage, layout.ToolTipText);
|
|
form.PlateView.Plate.Parts.Add(Rectangle());
|
|
form.PlateView.InvalidateOverlapCheck();
|
|
Assert.Equal("Layout: checking…", layout.Text);
|
|
Assert.Equal("Fill: 2 parts in 5 ms", status.Text);
|
|
|
|
bar.Bind(null);
|
|
Assert.False(layout.Available);
|
|
Assert.Equal("", layout.Text);
|
|
});
|
|
|
|
private static bool CornerIsEmpty(OverlayRun run)
|
|
{
|
|
using var image = new Bitmap(64, 64);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.Clear(Color.White);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
for (var x = 0; x < 64; x++)
|
|
for (var y = 0; y < 32; y++)
|
|
if (image.GetPixel(x, y).ToArgb() != Color.White.ToArgb())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
[Fact]
|
|
public void SnapshotKeepsOriginalLabelsWhenLiveNamesChange() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var task = run.Start();
|
|
var work = run.Next();
|
|
run.View.Plate.Parts[0].BaseDrawing.Name = "new label";
|
|
work.Complete();
|
|
run.Pump(task);
|
|
Assert.Equal("same name", Assert.Single(run.View.OverlapReport.Pairs).PartAName);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("remove")]
|
|
[InlineData("replace")]
|
|
[InlineData("clear")]
|
|
public void SlotChangingCollectionEventsImmediatelyDiscardCurrentReportAndPath(string edit) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.Finish();
|
|
using var image = new Bitmap(100, 100);
|
|
using var graphics = Graphics.FromImage(image);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
|
switch (edit)
|
|
{
|
|
case "remove": run.View.Plate.Parts.RemoveAt(0); break;
|
|
case "replace": run.View.Plate.Parts[0] = Rectangle(); break;
|
|
case "clear": run.View.Plate.Parts.Clear(); break;
|
|
}
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
});
|
|
|
|
[Fact]
|
|
public void AppendingPartRetainsTheExistingPairsPathButInvalidatesTheFullReport() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.Finish();
|
|
run.Paint();
|
|
var path = run.View.OverlapOverlay.CachedPath;
|
|
Assert.NotNull(path);
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.Null(run.View.OverlapReport);
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
run.Paint();
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void MenusFollowMdiActivationDocumentDisplayAndRunningState() => RunSta(() =>
|
|
{
|
|
using var host = new MenuHost();
|
|
host.Show();
|
|
var check = Menu(host, "mnuOverlapCheckActive");
|
|
var cancel = Menu(host, "mnuOverlapCancel");
|
|
var off = Menu(host, "mnuOverlapOff");
|
|
var areas = Menu(host, "mnuOverlapAreas");
|
|
var centroids = Menu(host, "mnuOverlapCentroids");
|
|
var both = Menu(host, "mnuOverlapBoth");
|
|
Assert.False(check.Enabled);
|
|
Assert.False(cancel.Enabled);
|
|
Assert.False(off.Enabled);
|
|
Assert.False(areas.Enabled);
|
|
Assert.False(centroids.Enabled);
|
|
Assert.False(both.Enabled);
|
|
Assert.All(new[] { check, cancel, off, areas, centroids, both }, item => Assert.Equal(Keys.None, item.ShortcutKeys));
|
|
|
|
using var first = new EditNestForm(new Nest("first")) { MdiParent = host };
|
|
// Manual-command test: an auto-check timer pumped during a wait would race its requests.
|
|
first.PlateView.SetOverlapAutoCheck(null);
|
|
first.Show();
|
|
Assert.True(check.Enabled);
|
|
Assert.True(areas.Checked);
|
|
off.PerformClick();
|
|
Assert.Equal(OverlapDisplayMode.Off, first.OverlapDisplay);
|
|
using var second = new EditNestForm(new Nest("second")) { MdiParent = host };
|
|
second.PlateView.SetOverlapAutoCheck(null);
|
|
second.Show();
|
|
Assert.True(areas.Checked);
|
|
Assert.False(off.Checked);
|
|
first.Activate();
|
|
Assert.True(off.Checked);
|
|
Assert.False(areas.Checked);
|
|
Assert.Equal(OverlapDisplayMode.Areas, second.OverlapDisplay);
|
|
centroids.PerformClick();
|
|
Assert.True(centroids.Checked);
|
|
Assert.False(off.Checked);
|
|
Assert.Equal(OverlapDisplayMode.Centroids, first.OverlapDisplay);
|
|
second.Activate();
|
|
both.PerformClick();
|
|
Assert.True(both.Checked);
|
|
Assert.False(centroids.Checked);
|
|
Assert.Equal(OverlapDisplayMode.Both, second.OverlapDisplay);
|
|
first.Activate();
|
|
Assert.True(centroids.Checked);
|
|
Assert.False(both.Checked);
|
|
off.PerformClick();
|
|
|
|
var previous = SynchronizationContext.Current;
|
|
var context = new PumpContext();
|
|
using var pending = new BlockingCollection<Work>();
|
|
SynchronizationContext.SetSynchronizationContext(context);
|
|
try
|
|
{
|
|
first.PlateView.OverlapOverlay.Analyze = (snapshot, token) =>
|
|
{
|
|
var request = new Work(snapshot, token);
|
|
pending.Add(request);
|
|
return request.WaitForResult();
|
|
};
|
|
var task = first.CheckOverlapsAsync();
|
|
Assert.True(pending.TryTake(out var request, TimeSpan.FromSeconds(15)));
|
|
Assert.True(cancel.Enabled);
|
|
Assert.True(areas.Checked); // Checking from Off selects Areas.
|
|
cancel.PerformClick();
|
|
Assert.False(cancel.Enabled);
|
|
Assert.True(request!.Token.IsCancellationRequested);
|
|
request.Complete();
|
|
context.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Canceled, first.PlateView.OverlapStatus);
|
|
}
|
|
finally
|
|
{
|
|
SynchronizationContext.SetSynchronizationContext(previous);
|
|
}
|
|
first.Dispose();
|
|
second.Dispose();
|
|
Assert.False(check.Enabled);
|
|
Assert.False(cancel.Enabled);
|
|
});
|
|
|
|
[Fact]
|
|
public void CheckCommandCapturesUnitsAndNamesUntilTheNextCheck() => RunSta(() =>
|
|
{
|
|
using var form = new EditNestForm(new Nest("units") { Units = Units.Millimeters });
|
|
form.PlateView.SetOverlapAutoCheck(null); // Manual-command test; see the menu test.
|
|
form.Show();
|
|
form.PlateView.Plate.Parts.Add(Rectangle());
|
|
form.PlateView.Plate.Parts.Add(Rectangle(1));
|
|
form.OverlapDisplay = OverlapDisplayMode.Both;
|
|
var previous = SynchronizationContext.Current;
|
|
var context = new PumpContext();
|
|
var started = new TaskCompletionSource<PlateOverlapSnapshot>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var result = new TaskCompletionSource<PlateOverlapReport>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
Task? task = null;
|
|
SynchronizationContext.SetSynchronizationContext(context);
|
|
try
|
|
{
|
|
form.PlateView.OverlapOverlay.Analyze = (snapshot, _) =>
|
|
{
|
|
started.TrySetResult(snapshot);
|
|
return WaitForWorkerResult(result.Task);
|
|
};
|
|
task = form.CheckOverlapsAsync();
|
|
var snapshot = WaitForSnapshot(started.Task);
|
|
form.Nest.Units = Units.Inches;
|
|
form.PlateView.Plate.Parts[0].BaseDrawing.Name = "changed";
|
|
result.SetResult(PlateOverlapAnalyzer.Analyze(snapshot));
|
|
context.Pump(task);
|
|
var center = form.PlateView.PointWorldToControl(form.PlateView.OverlapReport.Pairs[0].Marker);
|
|
form.PlateView.OverlapOverlay.UpdateHover(center);
|
|
Assert.Contains(" mm²", form.PlateView.OverlapOverlay.HoverText);
|
|
Assert.Contains("same name / same name", form.PlateView.OverlapOverlay.HoverText);
|
|
Assert.DoesNotContain("changed", form.PlateView.OverlapOverlay.HoverText);
|
|
form.PlateView.OverlapOverlay.Analyze = PlateOverlapAnalyzer.Analyze;
|
|
task = form.CheckOverlapsAsync();
|
|
Assert.Null(form.PlateView.OverlapOverlay.HoverText);
|
|
context.Pump(task);
|
|
form.PlateView.OverlapOverlay.UpdateHover(center);
|
|
Assert.Contains(" in²", form.PlateView.OverlapOverlay.HoverText);
|
|
Assert.Contains("changed / same name", form.PlateView.OverlapOverlay.HoverText);
|
|
Assert.Equal(OverlapDisplayMode.Both, form.OverlapDisplay);
|
|
}
|
|
finally
|
|
{
|
|
form.CancelOverlapCheck();
|
|
result.TrySetCanceled();
|
|
try { if (task != null) context.Pump(task); }
|
|
finally { SynchronizationContext.SetSynchronizationContext(previous); }
|
|
}
|
|
});
|
|
|
|
[Fact]
|
|
public void CleanupReleasesDequeuedWorkersEvenWithoutCompleteOrFail() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var first = run.Start();
|
|
var old = run.Next();
|
|
var second = run.Start();
|
|
var current = run.Next();
|
|
run.Dispose();
|
|
Assert.True(old.Result.Task.IsCompleted);
|
|
Assert.True(current.Result.Task.IsCompleted);
|
|
Assert.True(first.IsCompletedSuccessfully);
|
|
Assert.True(second.IsCompletedSuccessfully);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData(OverlapDisplayMode.Areas)]
|
|
[InlineData(OverlapDisplayMode.Centroids)]
|
|
[InlineData(OverlapDisplayMode.Both)]
|
|
public void RecheckPreservesEachVisibleModeAndDisplayOnlyRepaints(OverlapDisplayMode mode) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.OverlapDisplay = mode;
|
|
run.Finish();
|
|
Assert.Equal(mode, run.View.OverlapDisplay);
|
|
run.Finish();
|
|
Assert.Equal(mode, run.View.OverlapDisplay);
|
|
var report = run.View.OverlapReport;
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
foreach (var display in Enum.GetValues<OverlapDisplayMode>())
|
|
{
|
|
run.View.OverlapDisplay = display;
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Same(report, run.View.OverlapReport);
|
|
}
|
|
Assert.Equal(2, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void CentroidMarkerUsesGraphCoordinatesAndFixedScreenSizeAcrossZoom() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Centroids;
|
|
run.Finish();
|
|
var pair = Assert.Single(run.View.OverlapReport.Pairs);
|
|
using var image = new Bitmap(500, 500);
|
|
using var graphics = Graphics.FromImage(image);
|
|
foreach (var zoom in new[] { 20f, 2f })
|
|
{
|
|
run.View.ZoomToPoint(new Vector(), zoom);
|
|
run.View.SetOrigin(60, 320);
|
|
graphics.ResetTransform();
|
|
graphics.Clear(Color.White);
|
|
graphics.TranslateTransform(60, 320);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
|
var center = run.View.PointWorldToControl(pair.Marker);
|
|
var radius = (int)OverlapPairPresentation.MarkerHalfSize(run.View.DeviceDpi);
|
|
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(center.X - radius + 1, center.Y).ToArgb());
|
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(center.X - radius - 4, center.Y).ToArgb());
|
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(center.X - 60, center.Y).ToArgb());
|
|
run.View.MoveTo(new Point(center.X + (int)OverlapPairPresentation.HitRadius(run.View.DeviceDpi), center.Y));
|
|
Assert.Contains("Pair 1/2", run.View.OverlapOverlay.HoverText);
|
|
run.View.MoveTo(new Point(center.X + (int)OverlapPairPresentation.HitRadius(run.View.DeviceDpi) + 2, center.Y));
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
}
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void MarkerAndHoverUseThePoleOfTheSharedMaterialRatherThanBoundsCenter() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Clear();
|
|
var program = new Program(Mode.Absolute);
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(6, 0));
|
|
program.Codes.Add(new LinearMove(0, 3));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var drawing = new Drawing("triangle", program);
|
|
run.View.Plate.Parts.Add(new Part(drawing));
|
|
run.View.Plate.Parts.Add(new Part(drawing));
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Centroids;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 30);
|
|
run.View.SetOrigin(60, 220);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.Clear(Color.White);
|
|
graphics.TranslateTransform(60, 220);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
// The marker is the shared triangle's pole: its incenter (r, r), r = 2A / P.
|
|
var marker = Assert.Single(run.View.OverlapReport.Pairs).Marker;
|
|
var inradius = 2 * 9 / (6 + 3 + System.Math.Sqrt(45));
|
|
Assert.InRange(marker.X, inradius - 0.06, inradius + 0.06);
|
|
Assert.InRange(marker.Y, inradius - 0.06, inradius + 0.06);
|
|
var materialCenter = run.View.PointWorldToControl(marker);
|
|
var boundsCenter = run.View.PointWorldToControl(new Vector(6.0 / 2, 3.0 / 2));
|
|
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(materialCenter.X, materialCenter.Y).ToArgb());
|
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(boundsCenter.X, boundsCenter.Y).ToArgb());
|
|
run.View.MoveTo(materialCenter);
|
|
Assert.Contains("Pair 1/2", run.View.OverlapOverlay.HoverText);
|
|
run.View.MoveTo(boundsCenter);
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
});
|
|
|
|
[Fact]
|
|
public void BothRetainsAreaPathAndCoincidentHoverListsEveryPairWithoutChangingSelection() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Plate.Parts.Clear();
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
graphics.TranslateTransform(60, 220);
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
var path = run.View.OverlapOverlay.CachedPath;
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.View.OverlapOverlay.Draw(graphics);
|
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
|
var center = run.View.PointWorldToControl(new Vector(2, 2));
|
|
run.View.MoveTo(center);
|
|
var text = run.View.OverlapOverlay.HoverText!;
|
|
Assert.True(text.IndexOf("Pair 1/2", StringComparison.Ordinal) < text.IndexOf("Pair 1/3", StringComparison.Ordinal));
|
|
Assert.True(text.IndexOf("Pair 1/3", StringComparison.Ordinal) < text.IndexOf("Pair 2/3", StringComparison.Ordinal));
|
|
Assert.Empty(run.View.SelectedParts);
|
|
run.View.ClickAt(center);
|
|
Assert.NotEmpty(run.View.SelectedParts); // Informational hover does not consume the selection click.
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void PairDetailsOnlySuppressNormalTooltipWhileVisible() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
run.View.MoveTo(run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker));
|
|
var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
|
|
var hoverCheck = typeof(PlateView).GetMethod("HoverCheck", flags)!;
|
|
var normalVisible = typeof(PlateView).GetField("showTooltip", flags)!;
|
|
hoverCheck.Invoke(run.View, null); // Fire the existing normal-tooltip timer deterministically.
|
|
Assert.True((bool)normalVisible.GetValue(run.View)!);
|
|
using var image = new Bitmap(300, 300);
|
|
using var graphics = Graphics.FromImage(image);
|
|
Assert.True(run.View.OverlapOverlay.DrawHover(graphics));
|
|
Assert.True((bool)normalVisible.GetValue(run.View)!);
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Areas;
|
|
Assert.False(run.View.OverlapOverlay.DrawHover(graphics));
|
|
Assert.True((bool)normalVisible.GetValue(run.View)!); // PlateView now draws the original tooltip.
|
|
run.View.LeaveView();
|
|
hoverCheck.Invoke(run.View, null); // A queued timer cannot resurrect it after leave.
|
|
Assert.False((bool)normalVisible.GetValue(run.View)!);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("leave")]
|
|
[InlineData("stale-move")]
|
|
[InlineData("stale-paint")]
|
|
[InlineData("invalidate")]
|
|
[InlineData("mode")]
|
|
[InlineData("zoom")]
|
|
[InlineData("pan")]
|
|
[InlineData("resize")]
|
|
[InlineData("plate")]
|
|
[InlineData("restart-cancel")]
|
|
[InlineData("recreate")]
|
|
public void HoverClearsBeforeStaleDetailsCanBeShown(string change) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 20);
|
|
run.View.SetOrigin(60, 220);
|
|
var center = run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker);
|
|
run.View.MoveTo(center);
|
|
Assert.NotNull(run.View.OverlapOverlay.HoverText);
|
|
switch (change)
|
|
{
|
|
case "leave": run.View.LeaveView(); break;
|
|
case "stale-move":
|
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
|
run.View.MoveTo(center);
|
|
break;
|
|
case "stale-paint":
|
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
|
using (var image = new Bitmap(300, 300))
|
|
using (var graphics = Graphics.FromImage(image))
|
|
Assert.False(run.View.OverlapOverlay.DrawHover(graphics));
|
|
break;
|
|
case "invalidate": run.View.InvalidateOverlapCheck(); break;
|
|
case "mode": run.View.OverlapDisplay = OverlapDisplayMode.Centroids; break;
|
|
case "zoom": run.View.ZoomToPoint(new Vector(), 2); break;
|
|
case "pan": run.View.Pan(20, 10); break;
|
|
case "resize": run.View.Size = new System.Drawing.Size(300, 200); break;
|
|
case "plate": run.View.Plate = new Plate(); break;
|
|
case "restart-cancel":
|
|
var task = run.Start();
|
|
var work = run.Next();
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
run.View.CancelOverlapCheck();
|
|
work.Complete();
|
|
run.Pump(task);
|
|
break;
|
|
case "recreate": run.View.Recreate(); break;
|
|
}
|
|
Assert.Null(run.View.OverlapOverlay.HoverText);
|
|
});
|
|
|
|
[Fact]
|
|
public void CrowdedHoverPagesStayInViewportAndReachLastPairWithoutSelectionOrWheelCapture() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
PrepareCrowdedHover(run);
|
|
var report = run.View.OverlapReport;
|
|
var positions = run.View.Plate.Parts.Select(p => p.Location).ToArray();
|
|
run.View.SelectAll();
|
|
var selection = run.View.SelectedParts.ToArray();
|
|
using var image = new Bitmap(run.View.Width, run.View.Height);
|
|
using var graphics = Graphics.FromImage(image);
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
var pages = overlay.HoverPages;
|
|
Assert.True(pages.PageCount > 1);
|
|
var content = new List<string>();
|
|
for (var page = 0; page < pages.PageCount; page++)
|
|
{
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.True(new RectangleF(PointF.Empty, run.View.ClientSize).Contains(overlay.HoverBounds));
|
|
Assert.Equal(page, pages.PageIndex);
|
|
content.AddRange(pages.Lines);
|
|
Assert.Contains("PgUp/PgDn", string.Join("", pages.NavigationLines));
|
|
Assert.True(run.View.Command(Keys.PageDown));
|
|
}
|
|
Assert.Contains("Pair 11/12", string.Concat(content));
|
|
Assert.Contains(" in²", string.Concat(content));
|
|
Assert.Equal(overlay.HoverText.Replace("\n", ""), string.Concat(content));
|
|
Assert.Equal(pages.PageCount - 1, pages.PageIndex);
|
|
Assert.True(run.View.Command(Keys.PageUp));
|
|
Assert.Equal(pages.PageCount - 2, pages.PageIndex);
|
|
var lastIndex = pages.PageIndex;
|
|
Assert.False(run.View.Command(Keys.Control | Keys.PageDown));
|
|
Assert.Equal(lastIndex, pages.PageIndex);
|
|
run.View.MoveTo(run.View.PointWorldToControl(report.Pairs[0].Marker));
|
|
Assert.Equal(lastIndex, pages.PageIndex); // Moving within the same hit group retains the page.
|
|
Assert.Equal(selection, run.View.SelectedParts);
|
|
Assert.Equal(positions, run.View.Plate.Parts.Select(p => p.Location));
|
|
Assert.Same(report, run.View.OverlapReport);
|
|
Assert.Equal(1, run.Calls);
|
|
|
|
var scale = run.View.ViewScale;
|
|
run.View.WheelAt(run.View.PointWorldToControl(report.Pairs[0].Marker));
|
|
Assert.True(run.View.ViewScale > scale); // Wheel remains zoom, never paging.
|
|
Assert.Null(overlay.HoverText);
|
|
Assert.Null(overlay.HoverPages);
|
|
Assert.False(run.View.Command(Keys.PageDown));
|
|
Assert.Equal(selection, run.View.SelectedParts);
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void MovingDirectlyToAnotherCoincidentGroupStartsItsFirstPage() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Size = new System.Drawing.Size(300, 160);
|
|
run.View.Plate.Parts.Clear();
|
|
for (var i = 0; i < 12; i++)
|
|
run.View.Plate.Parts.Add(Rectangle(i < 6 ? 0 : 8));
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 15);
|
|
run.View.SetOrigin(60, 120);
|
|
run.View.MoveTo(run.View.PointWorldToControl(new Vector(2, 2)));
|
|
using var image = new Bitmap(300, 160);
|
|
using var graphics = Graphics.FromImage(image);
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.True(run.View.Command(Keys.PageDown));
|
|
Assert.Equal(1, overlay.HoverPages.PageIndex);
|
|
run.View.MoveTo(run.View.PointWorldToControl(new Vector(10, 2)));
|
|
Assert.Null(overlay.HoverPages);
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.NotNull(overlay.HoverPages);
|
|
Assert.Equal(0, overlay.HoverPages.PageIndex);
|
|
Assert.Contains("Pair 7/8", string.Concat(overlay.HoverPages.Lines));
|
|
Assert.Equal(1, run.Calls);
|
|
});
|
|
|
|
[Theory]
|
|
[InlineData("hover")]
|
|
[InlineData("leave")]
|
|
[InlineData("mode")]
|
|
[InlineData("stale-key")]
|
|
[InlineData("report")]
|
|
[InlineData("resize")]
|
|
[InlineData("font")]
|
|
[InlineData("click")]
|
|
[InlineData("drag")]
|
|
public void PagingResetsWithHoverLifetimeAndInactiveKeysAreNotConsumed(string change) => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
Assert.False(run.View.Command(Keys.PageDown));
|
|
PrepareCrowdedHover(run);
|
|
using var image = new Bitmap(run.View.Width, run.View.Height);
|
|
using var graphics = Graphics.FromImage(image);
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.True(run.View.Command(Keys.PageDown));
|
|
Assert.Equal(1, overlay.HoverPages.PageIndex);
|
|
var center = run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker);
|
|
using var largerFont = new Font(run.View.Font.FontFamily, run.View.Font.Size + 1);
|
|
switch (change)
|
|
{
|
|
case "hover": run.View.MoveTo(new Point(5, 5)); break;
|
|
case "leave": run.View.LeaveView(); break;
|
|
case "mode": run.View.OverlapDisplay = OverlapDisplayMode.Areas; break;
|
|
case "stale-key":
|
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
|
Assert.False(run.View.Command(Keys.PageDown));
|
|
break;
|
|
case "report": run.Finish(); break;
|
|
case "resize": run.View.Width += 10; break;
|
|
case "font": run.View.Font = largerFont; break;
|
|
case "click":
|
|
run.View.ClickAt(center);
|
|
Assert.NotEmpty(run.View.SelectedParts);
|
|
break;
|
|
case "drag": run.View.DragAt(center); break;
|
|
}
|
|
Assert.Null(overlay.HoverText);
|
|
Assert.Null(overlay.HoverPages);
|
|
Assert.False(run.View.Command(Keys.PageDown));
|
|
if (change == "stale-key")
|
|
run.Finish();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.View.MoveTo(center);
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.NotNull(overlay.HoverPages);
|
|
Assert.Equal(0, overlay.HoverPages.PageIndex);
|
|
});
|
|
|
|
[Fact]
|
|
public void LongNameAndTinyViewNeverSilentlyElideTheOnlyPair() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Size = new System.Drawing.Size(150, 120);
|
|
run.View.Plate.Parts[0].BaseDrawing.Name = new string('W', 250);
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 10);
|
|
run.View.SetOrigin(30, 90);
|
|
var center = run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker);
|
|
run.View.MoveTo(center);
|
|
using var image = new Bitmap(150, 120);
|
|
using var graphics = Graphics.FromImage(image);
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.False(overlay.HoverPages.NeedsLargerViewport);
|
|
Assert.True(overlay.HoverPages.PageCount > 1);
|
|
var content = new List<string>();
|
|
for (var i = 0; i < overlay.HoverPages.PageCount; i++)
|
|
{
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.True(new RectangleF(PointF.Empty, run.View.ClientSize).Contains(overlay.HoverBounds));
|
|
content.AddRange(overlay.HoverPages.Lines);
|
|
run.View.Command(Keys.PageDown);
|
|
}
|
|
Assert.Equal(overlay.HoverText.Replace("\n", ""), string.Concat(content));
|
|
run.View.Size = new System.Drawing.Size(40, 20);
|
|
overlay.UpdateHover(run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker));
|
|
Assert.True(overlay.DrawHover(graphics));
|
|
Assert.True(overlay.HoverPages.NeedsLargerViewport);
|
|
Assert.False(run.View.Command(Keys.PageDown));
|
|
});
|
|
|
|
[Fact]
|
|
public void AutoCheckRunsOnceTheLayoutSettlesAndRechecksAfterADrag() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.Status = "Fill: 2 parts in 5 ms";
|
|
run.EnableAutoCheck();
|
|
var overlay = run.View.OverlapOverlay;
|
|
Assert.True(overlay.IsAutoCheckScheduled);
|
|
Assert.Equal("Overlaps: check pending…", overlay.Message);
|
|
|
|
var task = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.False(overlay.IsAutoCheckScheduled);
|
|
Assert.Equal(1, run.Calls);
|
|
Assert.Equal("Fill: 2 parts in 5 ms", run.View.Status); // canvas label only
|
|
Assert.Equal("Overlaps: 1 pairs", overlay.Message);
|
|
|
|
// A drag changes only Part.Location: no collection event, so paint must notice it.
|
|
run.View.Plate.Parts[1].Offset(0.5, 0);
|
|
run.Paint();
|
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
|
Assert.True(overlay.IsAutoCheckScheduled);
|
|
run.View.Plate.Parts[1].Offset(0.5, 0); // still moving when the timer fires
|
|
Assert.Same(Task.CompletedTask, run.AutoTick());
|
|
Assert.True(overlay.IsAutoCheckScheduled); // waited another quiet period
|
|
Assert.Equal(1, run.Calls);
|
|
task = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.Equal(2, run.Calls);
|
|
});
|
|
|
|
[Fact]
|
|
public void AutoCheckWaitsForInteractionsAndRespectsCancel() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
var busy = true;
|
|
run.EnableAutoCheck(() => busy);
|
|
Assert.Same(Task.CompletedTask, run.AutoTick());
|
|
Assert.True(run.View.OverlapOverlay.IsAutoCheckScheduled);
|
|
Assert.Equal(0, run.Calls);
|
|
busy = false;
|
|
var task = run.AutoTick();
|
|
var work = run.Next();
|
|
run.View.CancelOverlapCheck();
|
|
work.Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Canceled, run.View.OverlapStatus);
|
|
run.Paint();
|
|
Assert.False(run.View.OverlapOverlay.IsAutoCheckScheduled); // no retry of a canceled layout
|
|
|
|
run.View.Plate.Parts.Add(Rectangle(2)); // but a new edit rechecks
|
|
Assert.True(run.View.OverlapOverlay.IsAutoCheckScheduled);
|
|
task = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.Equal(3, run.View.OverlapReport.Pairs.Count);
|
|
});
|
|
|
|
[Fact]
|
|
public void AutoCheckSupersedesARunningCheckWhenTheLayoutChanges() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.EnableAutoCheck();
|
|
var first = run.AutoTick();
|
|
var old = run.Next();
|
|
run.View.Plate.Parts[0].Offset(-10, 0); // user keeps editing while the check runs
|
|
run.Paint();
|
|
Assert.True(old.Token.IsCancellationRequested);
|
|
Assert.True(run.View.OverlapOverlay.IsAutoCheckScheduled);
|
|
old.Complete();
|
|
run.Pump(first);
|
|
Assert.Null(run.View.OverlapReport); // the old layout's result is never shown
|
|
var second = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(second);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.Empty(run.View.OverlapReport!.Pairs);
|
|
});
|
|
|
|
[Fact]
|
|
public void AutoCheckKeepsDisplayOffButManualCheckStillShowsAreasAndReportsToStatusBar() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.EnableAutoCheck();
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
|
var task = run.AutoTick();
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
|
Assert.Equal(OverlapDisplayMode.Off, run.View.OverlapDisplay);
|
|
|
|
run.View.Plate.Parts[1].Offset(0.25, 0);
|
|
task = run.Start(); // manual Check Active Plate
|
|
run.Next().Complete();
|
|
run.Pump(task);
|
|
Assert.Equal(OverlapDisplayMode.Areas, run.View.OverlapDisplay);
|
|
Assert.Equal("Overlaps: 1 pairs", run.View.Status);
|
|
Assert.False(run.View.OverlapOverlay.IsAutoCheckScheduled);
|
|
});
|
|
|
|
[Fact]
|
|
public void DefaultAnalyzerRechecksIncrementallyAndEditorsDropTheCache() => RunSta(() =>
|
|
{
|
|
using var run = new OverlayRun();
|
|
run.View.OverlapOverlay.Analyze = null; // production path: incremental real analyzer
|
|
run.View.Plate.Parts.Add(Rectangle(20));
|
|
run.View.Plate.Parts.Add(Rectangle(21));
|
|
var task = run.Start();
|
|
run.Pump(task);
|
|
var untouched = run.View.OverlapReport.Pairs.Single(pair => pair.PartAId == 2);
|
|
run.View.Plate.Parts[1].Offset(1, 0);
|
|
task = run.Start();
|
|
run.Pump(task);
|
|
Assert.Same(untouched.Regions, run.View.OverlapReport.Pairs.Single(pair => pair.PartAId == 2).Regions);
|
|
Assert.Equal(8, run.View.OverlapReport.Pairs.Single(pair => pair.PartAId == 0).Area, 9);
|
|
|
|
run.View.InvalidateOverlapCheck(); // converter may edit programs in place
|
|
task = run.Start();
|
|
run.Pump(task);
|
|
Assert.NotSame(untouched.Regions, run.View.OverlapReport.Pairs.Single(pair => pair.PartAId == 2).Regions);
|
|
});
|
|
|
|
[Fact]
|
|
public void EditNestFormEnablesAutoCheck() => RunSta(() =>
|
|
{
|
|
using var form = new EditNestForm(new Nest("auto"));
|
|
Assert.True(form.PlateView.OverlapOverlay.IsAutoCheckEnabled);
|
|
using var standalone = new PlateView();
|
|
Assert.False(standalone.OverlapOverlay.IsAutoCheckEnabled);
|
|
});
|
|
|
|
private static void PrepareCrowdedHover(OverlayRun run)
|
|
{
|
|
run.View.Size = new System.Drawing.Size(260, 160);
|
|
run.View.Plate.Parts.Clear();
|
|
for (var i = 0; i < 12; i++)
|
|
run.View.Plate.Parts.Add(Rectangle());
|
|
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
|
run.Finish();
|
|
run.View.ZoomToPoint(new Vector(), 15);
|
|
run.View.SetOrigin(60, 120);
|
|
run.View.MoveTo(run.View.PointWorldToControl(run.View.OverlapReport.Pairs[0].Marker));
|
|
}
|
|
|
|
// These waits are deliberate: only Task.Run workers use the result gate; the test's
|
|
// STA waits for capture delivery, then pumps every UI continuation explicitly.
|
|
private static PlateOverlapReport WaitForWorkerResult(Task<PlateOverlapReport> task) => task.GetAwaiter().GetResult();
|
|
|
|
private static PlateOverlapSnapshot WaitForSnapshot(Task<PlateOverlapSnapshot> task)
|
|
{
|
|
Assert.True(task.Wait(TimeSpan.FromSeconds(15)));
|
|
return task.GetAwaiter().GetResult();
|
|
}
|
|
|
|
private static ToolStripMenuItem Menu(MainForm host, string name) =>
|
|
(ToolStripMenuItem)host.MainMenuStrip!.Items.Find(name, true).Single();
|
|
|
|
private sealed class MenuHost : MainForm
|
|
{
|
|
// Do not run startup migrations/automatic new-document creation or save test window settings.
|
|
protected override void OnLoad(EventArgs e) { }
|
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
|
|
}
|
|
|
|
private sealed class TestView : PlateView
|
|
{
|
|
public void Pan(float x, float y) { origin.X += x; origin.Y += y; }
|
|
public void SetOrigin(float x, float y) => origin = new PointF(x, y);
|
|
public void Recreate() => RecreateHandle();
|
|
public void MoveTo(Point point) => OnMouseMove(new MouseEventArgs(MouseButtons.None, 0, point.X, point.Y, 0));
|
|
public void LeaveView() => OnMouseLeave(EventArgs.Empty);
|
|
public bool Command(Keys keys)
|
|
{
|
|
var message = new Message();
|
|
return ProcessCmdKey(ref message, keys);
|
|
}
|
|
public void WheelAt(Point point) => OnMouseWheel(new MouseEventArgs(MouseButtons.None, 0, point.X, point.Y, 120));
|
|
public void DragAt(Point point) => OnMouseMove(new MouseEventArgs(MouseButtons.Left, 0, point.X, point.Y, 0));
|
|
public void ClickAt(Point point)
|
|
{
|
|
OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, point.X, point.Y, 0));
|
|
OnMouseUp(new MouseEventArgs(MouseButtons.Left, 1, point.X, point.Y, 0));
|
|
}
|
|
}
|
|
|
|
private sealed class OverlayRun : IDisposable
|
|
{
|
|
private readonly PumpContext context = new();
|
|
private readonly SynchronizationContext? previous;
|
|
private readonly BlockingCollection<Work> work = new();
|
|
private readonly List<Work> outstanding = new();
|
|
private readonly List<Task> tasks = new();
|
|
private bool disposed;
|
|
public TestView View { get; } = new();
|
|
public int Calls;
|
|
|
|
public OverlayRun()
|
|
{
|
|
View.CreateControl();
|
|
previous = SynchronizationContext.Current;
|
|
SynchronizationContext.SetSynchronizationContext(context);
|
|
View.Plate.Parts.Add(Rectangle());
|
|
View.Plate.Parts.Add(Rectangle(1));
|
|
View.OverlapOverlay.Analyze = (snapshot, token) =>
|
|
{
|
|
Interlocked.Increment(ref Calls);
|
|
var request = new Work(snapshot, token);
|
|
lock (outstanding)
|
|
{
|
|
outstanding.Add(request);
|
|
if (disposed)
|
|
request.Release();
|
|
else
|
|
work.Add(request);
|
|
}
|
|
return request.WaitForResult();
|
|
};
|
|
}
|
|
|
|
public Task Start()
|
|
{
|
|
var task = View.CheckOverlapsAsync(Units.Inches);
|
|
tasks.Add(task);
|
|
return task;
|
|
}
|
|
public Work Next()
|
|
{
|
|
Assert.True(work.TryTake(out var result, TimeSpan.FromSeconds(15)), "Worker did not start.");
|
|
return result!;
|
|
}
|
|
public void Pump(Task task) => context.Pump(task);
|
|
public Task AutoTick()
|
|
{
|
|
var task = View.OverlapOverlay.RunAutoCheckTimer();
|
|
tasks.Add(task);
|
|
return task;
|
|
}
|
|
public void EnableAutoCheck(Func<bool>? busy = null)
|
|
{
|
|
View.OverlapOverlay.IsInteractionActive = busy ?? (() => false);
|
|
View.SetOverlapAutoCheck(() => Units.Inches);
|
|
}
|
|
public void Paint()
|
|
{
|
|
using var image = new Bitmap(64, 64);
|
|
using var graphics = Graphics.FromImage(image);
|
|
View.OverlapOverlay.Draw(graphics);
|
|
}
|
|
public void Finish()
|
|
{
|
|
var task = Start();
|
|
Next().Complete();
|
|
Pump(task);
|
|
}
|
|
public void Dispose()
|
|
{
|
|
if (disposed)
|
|
return;
|
|
try
|
|
{
|
|
View.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
lock (outstanding)
|
|
{
|
|
disposed = true;
|
|
foreach (var request in outstanding)
|
|
request.Release();
|
|
}
|
|
try { context.Pump(Task.WhenAll(tasks)); }
|
|
finally
|
|
{
|
|
SynchronizationContext.SetSynchronizationContext(previous);
|
|
work.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class Work(PlateOverlapSnapshot snapshot, CancellationToken token)
|
|
{
|
|
public CancellationToken Token { get; } = token;
|
|
public TaskCompletionSource<PlateOverlapReport> Result { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
// Only called by Task.Run workers; the STA supplies the result and pumps continuations.
|
|
public PlateOverlapReport WaitForResult() => Result.Task.GetAwaiter().GetResult();
|
|
public void Complete() => Result.SetResult(PlateOverlapAnalyzer.Analyze(snapshot));
|
|
public void Fail() => Result.SetException(new InvalidOperationException("test failure"));
|
|
// Cancellation is deliberately ignored while testing late completions. Cleanup alone
|
|
// releases every gate, including requests already taken from the pending queue.
|
|
public void Release() => Result.TrySetCanceled();
|
|
}
|
|
|
|
private sealed class PumpContext : SynchronizationContext
|
|
{
|
|
private readonly BlockingCollection<System.Action> callbacks = new();
|
|
public override void Post(SendOrPostCallback callback, object? state) => callbacks.Add(() => callback(state));
|
|
public void Pump(Task task)
|
|
{
|
|
while (!task.IsCompleted)
|
|
{
|
|
Assert.True(callbacks.TryTake(out var callback, TimeSpan.FromSeconds(15)), "UI continuation did not arrive.");
|
|
callback!();
|
|
}
|
|
task.GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
|
|
private static Part Rectangle(double x = 0)
|
|
{
|
|
var program = new Program(Mode.Absolute);
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(4, 0));
|
|
program.Codes.Add(new LinearMove(4, 4));
|
|
program.Codes.Add(new LinearMove(0, 4));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
return new Part(new Drawing("same name", program), new Vector(x, 0));
|
|
}
|
|
|
|
private static void RunSta(System.Action action) =>
|
|
StaTestThread.Run(action, TimeSpan.FromSeconds(60), "The STA test did not complete.");
|
|
}
|