feat(plateview): annotate overlap centroids
This commit is contained in:
@@ -266,11 +266,15 @@ public class PlateOverlapOverlayTests
|
||||
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.All(new[] { check, cancel, off, areas }, item => Assert.Equal(Keys.None, item.ShortcutKeys));
|
||||
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 };
|
||||
first.Show();
|
||||
@@ -286,6 +290,19 @@ public class PlateOverlapOverlayTests
|
||||
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();
|
||||
@@ -320,6 +337,458 @@ public class PlateOverlapOverlayTests
|
||||
Assert.False(cancel.Enabled);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void CheckCommandCapturesUnitsAndNamesUntilTheNextCheck() => RunSta(() =>
|
||||
{
|
||||
using var form = new EditNestForm(new Nest("units") { Units = Units.Millimeters });
|
||||
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].Centroid);
|
||||
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.Centroid);
|
||||
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 MarkerAndHoverUsePairMaterialCentroidRatherThanBoundsCenter() => 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);
|
||||
var materialCenter = run.View.PointWorldToControl(new Vector(6.0 / 3, 3.0 / 3));
|
||||
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].Centroid));
|
||||
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].Centroid);
|
||||
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].Centroid));
|
||||
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].Centroid));
|
||||
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].Centroid);
|
||||
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].Centroid);
|
||||
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].Centroid));
|
||||
Assert.True(overlay.DrawHover(graphics));
|
||||
Assert.True(overlay.HoverPages.NeedsLargerViewport);
|
||||
Assert.False(run.View.Command(Keys.PageDown));
|
||||
});
|
||||
|
||||
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].Centroid));
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -335,6 +804,20 @@ public class PlateOverlapOverlayTests
|
||||
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
|
||||
@@ -342,6 +825,9 @@ public class PlateOverlapOverlayTests
|
||||
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;
|
||||
|
||||
@@ -356,12 +842,24 @@ public class PlateOverlapOverlayTests
|
||||
{
|
||||
Interlocked.Increment(ref Calls);
|
||||
var request = new Work(snapshot, token);
|
||||
work.Add(request);
|
||||
lock (outstanding)
|
||||
{
|
||||
outstanding.Add(request);
|
||||
if (disposed)
|
||||
request.Release();
|
||||
else
|
||||
work.Add(request);
|
||||
}
|
||||
return request.WaitForResult();
|
||||
};
|
||||
}
|
||||
|
||||
public Task Start() => View.CheckOverlapsAsync();
|
||||
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.");
|
||||
@@ -376,9 +874,27 @@ public class PlateOverlapOverlayTests
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
View.Dispose();
|
||||
SynchronizationContext.SetSynchronizationContext(previous);
|
||||
work.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +906,9 @@ public class PlateOverlapOverlayTests
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user