Files
OpenNest/OpenNest.WinForms.Tests/Controls/CutSimulationOverlayTests.cs
T
ajandClaude Opus 5.5 9bf7760c1d fix(ui): keep the cut simulator's torch in place when rates change
Changing a rate, the pierce time or the machine rebuilt the timeline and
kept the playhead's old time. That time pointed at a different move on
the retimed timeline, so the torch jumped around while settings were
adjusted. When the new timeline has the same moves, Load now finds the
same move and how far through it the torch was (CutSimulation.TimeAt
inverts FractionAt, acceleration included).

The in-progress rapid was drawn whole with a dashed pen. GDI+ cost grows
with dash count: zoomed in, one paint of a long rapid can take seconds
to minutes. It is now clipped to the visible area, as finished rapids
already were.

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

150 lines
5.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 RetimingKeepsTheTorchWhereItWas() => RunSta(() =>
{
using var view = new PlateView { Plate = TwoSquares() };
var overlay = view.CutSimulation;
var slow = CutSimulation.Build(view.Plate, Parameters);
overlay.Load(slow);
var cut = slow.Steps.Last(s => s.Kind == CutSimulationStepKind.Cut);
overlay.Time = cut.StartTime + cut.Duration * 0.4;
var before = slow.ToolPositionAt(overlay.Time);
var fast = CutSimulation.Build(view.Plate, new CutParameters
{
Feedrate = 100,
RapidTravelRate = 6000,
PierceTime = TimeSpan.FromSeconds(1),
Units = Units.Inches,
});
overlay.Load(fast);
Assert.True(overlay.Time < cut.StartTime); // faster rapids moved it earlier
var after = fast.ToolPositionAt(overlay.Time);
Assert.Equal(before.X, after.X, 6);
Assert.Equal(before.Y, after.Y, 6);
});
[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.");
}