Files
OpenNest/OpenNest.WinForms.Tests/Controls/CutSimulationOverlayTests.cs
T
ajandClaude Opus 5.5 d1aa056b9b fix(ui): keep the cut simulator smooth while panning and zooming
The overlay re-stroked the whole finished trail on every paint: about
275 ms at zoom-to-fit on a 392-part plate and over 11 s zoomed in, where
dashed rapids spanned millions of on-screen dashes.

The finished trail is now cached in a bitmap at the current scale, so a
pan or playback frame blits it and draws only the moves since. A wheel
zoom stretches the cached image until the wheel has been still for
150 ms, then redraws it sharp. When the trail would be too large an
image, each paint draws only the moves on screen and clips straight
rapids to the view. Arcs are added as GDI+ arcs instead of 5-degree
polylines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:08:08 -04:00

124 lines
4.5 KiB
C#

using System.Drawing;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Geometry;
namespace OpenNest.WinForms.Tests.Controls;
public class CutSimulationOverlayTests
{
private static readonly CutParameters Parameters = new()
{
Feedrate = 100,
RapidTravelRate = 600,
PierceTime = TimeSpan.FromSeconds(1),
Units = Units.Inches,
};
private static Program Square(double size)
{
var program = new Program();
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(size, 0));
program.Codes.Add(new LinearMove(size, size));
program.Codes.Add(new LinearMove(0, size));
program.Codes.Add(new LinearMove(0, 0));
return program;
}
private static Plate TwoSquares()
{
var plate = new Plate(48, 96);
plate.Parts.Add(new Part(new Drawing("a", Square(10)), new Vector(5, 5)));
plate.Parts.Add(new Part(new Drawing("b", Square(10)), new Vector(25, 5)));
return plate;
}
[Fact]
public void PartsAheadOfTheTorchArePending() => RunSta(() =>
{
using var view = new PlateView { Plate = TwoSquares() };
var overlay = view.CutSimulation;
var sim = CutSimulation.Build(view.Plate, Parameters);
overlay.Load(sim);
Assert.True(overlay.IsPartPending(0)); // still travelling to the first part
var firstCut = sim.Steps.First(s => s.Kind == CutSimulationStepKind.Cut);
overlay.Time = firstCut.StartTime + firstCut.Duration / 2;
Assert.False(overlay.IsPartPending(0));
Assert.True(overlay.IsPartPending(1));
overlay.Time = sim.TotalTime;
Assert.False(overlay.IsPartPending(1));
});
[Fact]
public void FinishedCutsAreDrawnOnlyBehindThePlayhead() => RunSta(() =>
{
using var view = new PlateView { Plate = TwoSquares() };
var sim = CutSimulation.Build(view.Plate, Parameters);
view.CutSimulation.Load(sim);
var secondPartCut = sim.Steps.First(s => s.PartIndex == 1 && s.Kind == CutSimulationStepKind.Cut);
view.CutSimulation.Time = secondPartCut.StartTime;
using var bitmap = new Bitmap(400, 400);
using var graphics = Graphics.FromImage(bitmap);
graphics.TranslateTransform(20, 380);
graphics.Clear(Color.Black);
view.CutSimulation.Draw(graphics);
// Bottom edge midpoints of each square, in graph coordinates.
var cut = Pixel(view, bitmap, new Vector(10, 5));
var uncut = Pixel(view, bitmap, new Vector(35, 15));
Assert.True(cut.R > 200 && cut.G < 160, $"first part trail missing: {cut}");
Assert.Equal(Color.Black.ToArgb(), uncut.ToArgb());
});
[Fact]
public void ZoomedInTrailIsDrawnWherePannedTo() => RunSta(() =>
{
using var view = new PlateView { Plate = TwoSquares() };
var sim = CutSimulation.Build(view.Plate, Parameters);
view.CutSimulation.Load(sim);
view.CutSimulation.Time = sim.TotalTime;
view.ZoomToPoint(new Vector(10, 5), 100 / view.ViewScale, false); // trail far larger than the screen
using var bitmap = new Bitmap(400, 400);
using var graphics = Graphics.FromImage(bitmap);
foreach (var target in new[] { new Vector(10, 5), new Vector(35, 15) })
{
var point = view.PointWorldToGraph(target);
graphics.ResetTransform();
graphics.Clear(Color.Black);
graphics.TranslateTransform(200 - point.X, 200 - point.Y);
view.CutSimulation.Draw(graphics);
var pixel = bitmap.GetPixel(200, 200);
Assert.True(pixel.R > 200 && pixel.G < 160, $"trail missing at {target}: {pixel}");
}
});
[Fact]
public void PlateSwitchClearsTheSimulation() => RunSta(() =>
{
using var view = new PlateView { Plate = TwoSquares() };
view.CutSimulation.Load(CutSimulation.Build(view.Plate, Parameters));
view.CutSimulation.Play();
Assert.True(view.CutSimulation.IsPlaying);
view.Plate = new Plate();
Assert.False(view.CutSimulation.IsActive);
Assert.False(view.CutSimulation.IsPlaying);
});
private static Color Pixel(PlateView view, Bitmap bitmap, Vector world)
{
var point = view.PointWorldToGraph(world);
return bitmap.GetPixel((int)System.Math.Round(point.X + 20), (int)System.Math.Round(point.Y + 380));
}
private static void RunSta(System.Action action) =>
StaTestThread.Run(action, TimeSpan.FromSeconds(30), "Cut simulation overlay test timed out.");
}