mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 17:11:06 -04:00
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>
327 lines
13 KiB
C#
327 lines
13 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CNC;
|
|
|
|
public class CutSimulationTests
|
|
{
|
|
private static readonly CutParameters Parameters = new()
|
|
{
|
|
Feedrate = 100, // in/min
|
|
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 PlateWith(params (Program Program, Vector Location)[] parts)
|
|
{
|
|
var plate = new Plate(48, 96);
|
|
foreach (var (program, location) in parts)
|
|
plate.Parts.Add(new Part(new Drawing("part", program), location));
|
|
return plate;
|
|
}
|
|
|
|
[Fact]
|
|
public void TimeAtInvertsFractionAtUnderAcceleration()
|
|
{
|
|
var plate = PlateWith((Square(10), new Vector(30, 40)));
|
|
var sim = CutSimulation.Build(plate, new CutParameters
|
|
{
|
|
Feedrate = 100,
|
|
RapidTravelRate = 600,
|
|
PierceTime = TimeSpan.FromSeconds(1),
|
|
Units = Units.Inches,
|
|
Motion = new MotionProfile { CutAcceleration = 2, RapidAcceleration = 4, CornerDeviation = 0.001 },
|
|
});
|
|
|
|
for (var index = 0; index < sim.Steps.Count; index++)
|
|
{
|
|
foreach (var fraction in new[] { 0, 0.1, 0.5, 0.93, 1 })
|
|
{
|
|
var time = sim.TimeAt(index, fraction);
|
|
Assert.InRange(time, sim.Steps[index].StartTime, sim.Steps[index].EndTime);
|
|
if (time < sim.Steps[index].EndTime)
|
|
Assert.Equal(fraction, sim.FractionAt(index, time), 6);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SquareIsRapidPierceThenFourTimedCuts()
|
|
{
|
|
var plate = PlateWith((Square(10), new Vector(30, 40)));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
CutSimulationStepKind.Rapid,
|
|
CutSimulationStepKind.Pierce,
|
|
CutSimulationStepKind.Cut,
|
|
CutSimulationStepKind.Cut,
|
|
CutSimulationStepKind.Cut,
|
|
CutSimulationStepKind.Cut,
|
|
},
|
|
sim.Steps.Select(s => s.Kind));
|
|
Assert.Equal(Vector.Zero, sim.Steps[0].Start);
|
|
Assert.Equal(new Vector(30, 40), sim.Steps[0].End);
|
|
Assert.Equal(new Vector(40, 40), sim.Steps[2].End);
|
|
|
|
var expected = TimeSpan.FromMinutes(50.0 / 600) + TimeSpan.FromSeconds(1) + TimeSpan.FromMinutes(40.0 / 100);
|
|
Assert.Equal(expected.TotalSeconds, sim.TotalTime.TotalSeconds, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void SecondPartTravelsFromWhereTheFirstFinished()
|
|
{
|
|
var plate = PlateWith((Square(10), new Vector(0, 0)), (Square(5), new Vector(20, 0)));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
var travel = sim.Steps.Single(s => s.Kind == CutSimulationStepKind.Rapid && s.PartIndex == 1);
|
|
Assert.Equal(new Vector(0, 0), travel.Start);
|
|
Assert.Equal(new Vector(20, 0), travel.End);
|
|
Assert.Equal(2, sim.Steps.Count(s => s.Kind == CutSimulationStepKind.Pierce));
|
|
}
|
|
|
|
[Fact]
|
|
public void RapidInPlaceStillEndsTheCut()
|
|
{
|
|
// The second part starts exactly where the first one finished.
|
|
var plate = PlateWith((Square(10), Vector.Zero), (Square(10), Vector.Zero));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
Assert.Equal(2, sim.Steps.Count(s => s.Kind == CutSimulationStepKind.Pierce));
|
|
Assert.DoesNotContain(sim.Steps, s => s.Kind == CutSimulationStepKind.Rapid);
|
|
}
|
|
|
|
[Fact]
|
|
public void UncutLayersTravelHeadUpAndRepierce()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(4, 0));
|
|
program.Codes.Add(new LinearMove(6, 0) { Layer = LayerType.Tab });
|
|
program.Codes.Add(new LinearMove(10, 0));
|
|
var plate = PlateWith((program, Vector.Zero));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
CutSimulationStepKind.Pierce,
|
|
CutSimulationStepKind.Cut,
|
|
CutSimulationStepKind.Rapid,
|
|
CutSimulationStepKind.Pierce,
|
|
CutSimulationStepKind.Cut,
|
|
},
|
|
sim.Steps.Select(s => s.Kind));
|
|
}
|
|
|
|
[Fact]
|
|
public void FullCircleArcSweepsOnceInItsDirection()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(2, 0));
|
|
program.Codes.Add(new ArcMove(new Vector(2, 0), new Vector(0, 0), RotationType.CW));
|
|
var plate = PlateWith((program, Vector.Zero));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
var arc = sim.Steps.Single(s => s.IsArc);
|
|
Assert.Equal(-2 * System.Math.PI, arc.Sweep, 9);
|
|
Assert.Equal(4 * System.Math.PI, arc.Length, 9);
|
|
var quarter = arc.PointAt(0.25);
|
|
Assert.Equal(0, quarter.X, 9);
|
|
Assert.Equal(-2, quarter.Y, 9); // clockwise from (2, 0)
|
|
}
|
|
|
|
[Fact]
|
|
public void ToolPositionInterpolatesAcrossTheTimeline()
|
|
{
|
|
var plate = PlateWith((Square(10), Vector.Zero));
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
var firstCut = sim.Steps.First(s => s.Kind == CutSimulationStepKind.Cut);
|
|
|
|
var halfway = firstCut.StartTime + firstCut.Duration / 2;
|
|
|
|
Assert.Equal(new Vector(5, 0), sim.ToolPositionAt(halfway));
|
|
Assert.Equal(sim.Steps.ToList().IndexOf(firstCut), sim.IndexAt(halfway));
|
|
Assert.Equal(sim.Steps.Count, sim.IndexAt(sim.TotalTime));
|
|
Assert.Equal(Vector.Zero, sim.ToolPositionAt(sim.TotalTime));
|
|
}
|
|
|
|
[Fact]
|
|
public void MillimeterRatesArePerSecond()
|
|
{
|
|
var plate = PlateWith((Square(100), Vector.Zero));
|
|
var parameters = new CutParameters
|
|
{
|
|
Feedrate = 50,
|
|
RapidTravelRate = 500,
|
|
PierceTime = TimeSpan.Zero,
|
|
Units = Units.Millimeters,
|
|
};
|
|
|
|
var sim = CutSimulation.Build(plate, parameters);
|
|
|
|
Assert.Equal(8, sim.TotalTime.TotalSeconds, 6); // 400 mm at 50 mm/s; the rapid is zero length
|
|
}
|
|
|
|
private static Program SquareWithHole()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(6, 5));
|
|
program.Codes.Add(new ArcMove(new Vector(6, 5), new Vector(5, 5), RotationType.CW));
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(10, 0));
|
|
program.Codes.Add(new LinearMove(10, 10));
|
|
program.Codes.Add(new LinearMove(0, 10));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
return program;
|
|
}
|
|
|
|
[Fact]
|
|
public void ContoursAndPartsStartAtTheirPierces()
|
|
{
|
|
var plate = PlateWith((SquareWithHole(), Vector.Zero), (SquareWithHole(), new Vector(20, 0)));
|
|
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
|
|
var pierces = sim.Steps.Where(s => s.Kind == CutSimulationStepKind.Pierce).Select(s => s.StartTime).ToArray();
|
|
Assert.Equal(4, pierces.Length);
|
|
Assert.Equal(pierces, sim.ContourStarts);
|
|
Assert.Equal(new[] { pierces[0], pierces[2] }, sim.PartStarts);
|
|
}
|
|
|
|
[Fact]
|
|
public void JumpsMoveBetweenContoursAndParts()
|
|
{
|
|
var plate = PlateWith((SquareWithHole(), Vector.Zero), (SquareWithHole(), new Vector(20, 0)));
|
|
var sim = CutSimulation.Build(plate, Parameters);
|
|
var contours = sim.ContourStarts;
|
|
|
|
Assert.Equal(contours[0], sim.NextContour(TimeSpan.Zero));
|
|
Assert.Equal(contours[2], sim.NextContour(contours[1]));
|
|
Assert.Equal(sim.TotalTime, sim.NextContour(contours[3]));
|
|
Assert.Equal(sim.PartStarts[1], sim.NextPart(contours[0]));
|
|
Assert.Equal(sim.TotalTime, sim.NextPart(sim.PartStarts[1]));
|
|
|
|
// Mid-contour goes back to that contour's start; at a start it goes to the one before.
|
|
var midSecond = contours[1] + TimeSpan.FromSeconds(2);
|
|
Assert.Equal(contours[1], sim.PreviousContour(midSecond));
|
|
Assert.Equal(contours[0], sim.PreviousContour(contours[1]));
|
|
Assert.Equal(TimeSpan.Zero, sim.PreviousContour(contours[0]));
|
|
Assert.Equal(sim.PartStarts[0], sim.PreviousPart(sim.PartStarts[1]));
|
|
Assert.Equal(sim.PartStarts[1], sim.PreviousPart(sim.TotalTime));
|
|
}
|
|
|
|
// Clockwise outside (exterior for the CI Fiber post) around a counterclockwise hole.
|
|
private static Program PlateWithHole(LayerType layer = LayerType.Cut)
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(4, 5));
|
|
program.Codes.Add(new LinearMove(6, 5) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(6, 7) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(4, 7) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(4, 5) { Layer = layer });
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(0, 10) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(10, 10) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(10, 0) { Layer = layer });
|
|
program.Codes.Add(new LinearMove(0, 0) { Layer = layer });
|
|
return program;
|
|
}
|
|
|
|
[Fact]
|
|
public void DisplayLayerContourMovesAreCut()
|
|
{
|
|
// Cutting strategies emit contour moves on the Display layer; the posts cut them.
|
|
var sim = CutSimulation.Build(PlateWith((PlateWithHole(LayerType.Display), Vector.Zero)), Parameters);
|
|
|
|
Assert.Equal(8, sim.Steps.Count(s => s.Kind == CutSimulationStepKind.Cut));
|
|
Assert.Equal(2, sim.Steps.Count(s => s.Kind == CutSimulationStepKind.Pierce));
|
|
}
|
|
|
|
[Fact]
|
|
public void SuppressedMovesAreLeftOut()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(5, 0));
|
|
program.Codes.Add(new LinearMove(10, 0) { Suppressed = true });
|
|
program.Codes.Add(new LinearMove(10, 5));
|
|
|
|
var sim = CutSimulation.Build(PlateWith((program, Vector.Zero)), Parameters);
|
|
|
|
Assert.Equal(10, sim.Steps.Where(s => s.Kind == CutSimulationStepKind.Cut).Sum(s => s.Length), 9);
|
|
Assert.Single(sim.Steps, s => s.Kind == CutSimulationStepKind.Rapid && s.Start == new Vector(5, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void HolesUseTheHoleFeedAndOutsidesTheFeed()
|
|
{
|
|
var parameters = new CutParameters
|
|
{
|
|
Feedrate = 100,
|
|
HoleFeedrate = 50,
|
|
RapidTravelRate = 600,
|
|
Units = Units.Inches,
|
|
};
|
|
|
|
var sim = CutSimulation.Build(PlateWith((PlateWithHole(), Vector.Zero)), parameters);
|
|
|
|
var cuts = sim.Steps.Where(s => s.Kind == CutSimulationStepKind.Cut).ToList();
|
|
Assert.All(cuts.Take(4), s => Assert.True(s.IsHole));
|
|
Assert.All(cuts.Skip(4), s => Assert.False(s.IsHole));
|
|
Assert.Equal(TimeSpan.FromMinutes(8.0 / 50).TotalSeconds, cuts.Take(4).Sum(s => s.Duration.TotalSeconds), 6);
|
|
Assert.Equal(TimeSpan.FromMinutes(40.0 / 100).TotalSeconds, cuts.Skip(4).Sum(s => s.Duration.TotalSeconds), 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void SkippedEtchContoursLeaveNoTimeAndRejoinTravel()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(20, 0));
|
|
program.Codes.Add(new LinearMove(25, 0) { Layer = LayerType.Scribe });
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(10, 0));
|
|
var plate = PlateWith((program, Vector.Zero));
|
|
var parameters = new CutParameters { Feedrate = 100, RapidTravelRate = 600, Units = Units.Inches };
|
|
|
|
var kept = CutSimulation.Build(plate, parameters);
|
|
parameters.SkipScribe = true;
|
|
var skipped = CutSimulation.Build(plate, parameters);
|
|
|
|
Assert.Contains(kept.Steps, s => s.Kind == CutSimulationStepKind.Scribe);
|
|
Assert.DoesNotContain(skipped.Steps, s => s.Kind == CutSimulationStepKind.Scribe);
|
|
Assert.DoesNotContain(skipped.Steps, s => s.Kind == CutSimulationStepKind.Rapid); // straight from the origin to the cut
|
|
Assert.Single(skipped.ContourStarts);
|
|
Assert.Equal(TimeSpan.FromMinutes(10.0 / 100).TotalSeconds, skipped.TotalTime.TotalSeconds, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPlateHasNoTimeline()
|
|
{
|
|
var sim = CutSimulation.Build(new Plate(48, 96), Parameters);
|
|
|
|
Assert.Empty(sim.Steps);
|
|
Assert.Equal(TimeSpan.Zero, sim.TotalTime);
|
|
Assert.Equal(-1, sim.IndexAt(TimeSpan.Zero));
|
|
}
|
|
}
|