mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 02:15:07 -05:00
1055 lines
46 KiB
C#
1055 lines
46 KiB
C#
using System.Reflection;
|
|
using OpenNest.Clamps;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Tests.CutOffs;
|
|
|
|
public class AutomaticCutOffPlannerTests
|
|
{
|
|
private static Plate MakePlate(int quadrant = 1, double occupied = 80)
|
|
{
|
|
var plate = new Plate(81, 120) { Quadrant = quadrant, PartSpacing = 0.5, Quantity = 2 };
|
|
var x = quadrant is 2 or 3 ? -occupied : 10;
|
|
var y = quadrant is 3 or 4 ? -40 : 20;
|
|
plate.Parts.Add(Rectangle(occupied - 10, 20, new Vector(x, y)));
|
|
return plate;
|
|
}
|
|
|
|
private static Part Rectangle(double length, double width, Vector location)
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(length, 0));
|
|
program.Codes.Add(new LinearMove(length, width));
|
|
program.Codes.Add(new LinearMove(0, width));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var drawing = new Drawing("rectangle", program);
|
|
Assert.Equal(length * width, drawing.Area, 8);
|
|
return new Part(drawing, location);
|
|
}
|
|
|
|
private static AutomaticCutOffPlan Plan(Plate plate, double spacing = 35,
|
|
CutOffSettings? settings = null) => AutomaticCutOffPlanner.Create(
|
|
plate, new AutomaticCutOffOptions { Spacing = spacing }, settings ?? new CutOffSettings());
|
|
|
|
private static double[] Positions(AutomaticCutOffPlan plan) =>
|
|
plan.Definitions.Select(c => c.Position.X).ToArray();
|
|
|
|
private static void AssertPositions(double[] expected, AutomaticCutOffPlan plan)
|
|
{
|
|
var actual = Positions(plan);
|
|
Assert.Equal(expected.Length, actual.Length);
|
|
for (var index = 0; index < expected.Length; index++)
|
|
Assert.Equal(expected[index], actual[index], 9);
|
|
}
|
|
|
|
private static (Vector From, Vector To)[] Segments(Program program) =>
|
|
Enumerable.Range(0, program.Codes.Count / 2).Select(i => (
|
|
Assert.IsType<RapidMove>(program.Codes[2 * i]).EndPoint,
|
|
Assert.IsType<LinearMove>(program.Codes[2 * i + 1]).EndPoint)).ToArray();
|
|
|
|
[Fact]
|
|
public void UsedEnvelope_StopsBeforeUnusedTail_AndPreviewsAreDetached()
|
|
{
|
|
var plate = MakePlate();
|
|
var plan = Plan(plate);
|
|
var separator = 80 + 0.5 + Tolerance.Epsilon;
|
|
|
|
Assert.Equal(new[] { 35, 70, separator }, Positions(plan));
|
|
Assert.All(plan.Definitions, c => Assert.Equal(CutOffAxis.Vertical, c.Axis));
|
|
Assert.Equal(80, plan.OccupiedSpan);
|
|
Assert.Equal(separator, plan.UsedSpan);
|
|
Assert.Equal(120 - separator, plan.TailLength);
|
|
Assert.Equal(separator, plan.TailSeparatorX);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.False(plan.HasBlockingDiagnostics);
|
|
Assert.Equal(plan.Definitions.Count, plan.PreviewParts.Count);
|
|
for (var i = 0; i < plan.Definitions.Count; i++)
|
|
{
|
|
Assert.Same(plan.Definitions[i].Drawing, plan.PreviewParts[i].BaseDrawing);
|
|
Assert.DoesNotContain(plan.PreviewParts[i], plate.Parts);
|
|
}
|
|
var segment = Assert.Single(Segments(plan.Definitions[^1].Drawing.Program));
|
|
Assert.Equal(new Vector(separator, 0), segment.From);
|
|
Assert.Equal(new Vector(separator, 81), segment.To);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void MinimumTail_SkippedSeparatorExtendsSkeletonGridToSheetEnd(int quadrant)
|
|
{
|
|
// The proposed separator is just before X=105, with a tail shorter than 16.
|
|
// Skipping it leaves the tail on the skeleton, so the grid must continue with
|
|
// the line at X=105; otherwise the final strip would run 70 to 120.
|
|
var plate = MakePlate(quadrant, occupied: 104);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
var options = new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 16 };
|
|
|
|
var plan = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings());
|
|
|
|
Assert.Equal(new[] { 35.0, 70, 105 }.Select(x => sign * x), Positions(plan));
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Null(plan.TailSeparatorX);
|
|
Assert.Equal(0, plan.TailLength);
|
|
Assert.Equal(120, plan.UsedSpan);
|
|
Assert.Contains(plan.Diagnostics, d => d.Message.Contains("minimum tail length"));
|
|
Assert.Empty(plate.CutOffs);
|
|
Assert.Single(plate.Parts);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, null)]
|
|
[InlineData(1, 17.5)]
|
|
[InlineData(2, 17.5)]
|
|
[InlineData(3, 17.5)]
|
|
[InlineData(4, 17.5)]
|
|
public void MinimumTail_SkippedSeparatorKeepsEveryStripWithinSpacing(int quadrant, double? minimumStrip)
|
|
{
|
|
// A 144-long sheet used through 139.75 proposes a separator at 140 with a 4-long
|
|
// tail. With the separator skipped, no strip may exceed the 35 spacing.
|
|
var plate = new Plate(60, 144) { Quadrant = quadrant, PartSpacing = 0.25 };
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
plate.Parts.Add(Rectangle(138, 8, new Vector(quadrant is 2 or 3 ? -139.75 : 1.75,
|
|
quadrant is 3 or 4 ? -10 : 2)));
|
|
var options = new AutomaticCutOffOptions
|
|
{
|
|
Spacing = 35,
|
|
MinimumStripLength = minimumStrip,
|
|
MinimumTailLength = 12,
|
|
};
|
|
|
|
var plan = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings());
|
|
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.TailBelowMinimum);
|
|
var boundaries = Positions(plan).Select(x => sign * x).Prepend(0).Append(144).OrderBy(x => x).ToArray();
|
|
Assert.Equal(6, boundaries.Length);
|
|
Assert.All(boundaries.Zip(boundaries.Skip(1), (a, b) => b - a),
|
|
strip => Assert.InRange(strip, Tolerance.Epsilon, 35 + Tolerance.Epsilon));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, -0.01, false)]
|
|
[InlineData(1, 0, true)]
|
|
[InlineData(1, 0.01, true)]
|
|
[InlineData(2, -0.01, false)]
|
|
[InlineData(2, 0, true)]
|
|
[InlineData(2, 0.01, true)]
|
|
[InlineData(3, -0.01, false)]
|
|
[InlineData(3, 0, true)]
|
|
[InlineData(3, 0.01, true)]
|
|
[InlineData(4, -0.01, false)]
|
|
[InlineData(4, 0, true)]
|
|
[InlineData(4, 0.01, true)]
|
|
public void MinimumTail_TwelveInchesIsInclusiveAfterSeparatorClearance(int quadrant, double extra, bool keep)
|
|
{
|
|
var occupied = 120 - 12 - 0.5 - Tolerance.Epsilon - extra;
|
|
var plate = MakePlate(quadrant, occupied);
|
|
var options = new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 12 };
|
|
|
|
var plan = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings());
|
|
|
|
Assert.Equal(keep, plan.HasSeparatedTail);
|
|
Assert.Equal(keep ? 4 : 3, plan.Definitions.Count);
|
|
if (keep)
|
|
Assert.Equal(12 + extra, plan.TailLength, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void MinimumTail_DoesNotRemoveExistingSeparatorWhenRerunWithHigherMinimum()
|
|
{
|
|
var plate = MakePlate(occupied: 110);
|
|
var settings = new CutOffSettings();
|
|
var original = Plan(plate);
|
|
plate.CutOffs.AddRange(original.Definitions);
|
|
plate.RegenerateCutOffs(settings);
|
|
var definitions = plate.CutOffs.ToArray();
|
|
var parts = plate.Parts.ToArray();
|
|
var programs = definitions.Select(c => c.Drawing.Program).ToArray();
|
|
|
|
var plan = AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 12 }, settings);
|
|
|
|
Assert.Empty(plan.Definitions);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Equal(definitions, plate.CutOffs.ToArray());
|
|
Assert.Equal(parts, plate.Parts.ToArray());
|
|
Assert.Equal(programs, plate.CutOffs.Select(c => c.Drawing.Program));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
public void MinimumTail_InvalidValueIsRejected(double minimum)
|
|
{
|
|
var plate = MakePlate();
|
|
Assert.Throws<ArgumentException>(() => AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = minimum }, new CutOffSettings()));
|
|
Assert.Empty(plate.CutOffs);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2, 0.5)]
|
|
[InlineData(0.5, 2)]
|
|
[InlineData(2, 2)]
|
|
[InlineData(0, 0)]
|
|
public void TailUsesGreaterClearance_NotSum(double partSpacing, double clearance)
|
|
{
|
|
var plate = MakePlate();
|
|
plate.PartSpacing = partSpacing;
|
|
var plan = Plan(plate, settings: new CutOffSettings { PartClearance = clearance });
|
|
Assert.Equal(80 + System.Math.Max(partSpacing, clearance) + Tolerance.Epsilon,
|
|
plan.TailSeparatorX);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, 1)]
|
|
[InlineData(2, -1)]
|
|
[InlineData(3, -1)]
|
|
[InlineData(4, 1)]
|
|
public void AllQuadrants_AdvanceXFromOrigin_AcrossPhysicalWidth(int quadrant, int sign)
|
|
{
|
|
var plate = MakePlate(quadrant);
|
|
plate.EdgeSpacing = new Spacing(10, 10);
|
|
var settings = new CutOffSettings { Overtravel = 3 };
|
|
var plan = Plan(plate, settings: settings);
|
|
Assert.Equal(new[] { sign * 35.0, sign * 70.0, sign * (80.5 + Tolerance.Epsilon) },
|
|
Positions(plan));
|
|
var bounds = plate.BoundingBox(false);
|
|
var segment = Assert.Single(Segments(plan.Definitions[^1].Drawing.Program));
|
|
Assert.Equal(bounds.Bottom, segment.From.Y);
|
|
Assert.Equal(bounds.Top + 3, segment.To.Y);
|
|
Assert.Equal(80.5 + Tolerance.Epsilon, plan.UsedSpan);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void ClampWall_SkipsNominalLineAndMovesTailSeparatorPastClamp(int quadrant)
|
|
{
|
|
var plate = MakePlate(quadrant);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
// Real rail zones are stored in the sheet's lower-left frame in every quadrant.
|
|
// The tail clamp covers 80.6..80.8; with 0.25 clearance the separator moves to 81.05.
|
|
plate.SetClampRail(new ClampRail(0.2, 0.625, new[] { 35.2, 80.7 }));
|
|
var before = plate.Parts.ToArray();
|
|
var plan = Plan(plate);
|
|
var separator = 81.05 + Tolerance.Epsilon;
|
|
AssertPositions(new[] { sign * 70.0, sign * separator }, plan);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.Equal(120 - separator, plan.TailLength, 9);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampMovedTailSeparator);
|
|
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
|
|
Assert.Equal(before, plate.Parts.ToArray());
|
|
Assert.Empty(plate.CutOffs);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClampOnlyBlocksItsInflatedSpan_AndRemovedClampStopsBlocking()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.ClampZones.Add(new ClampZone("front", new Box(35.25, 0, 1.5, 0.625)));
|
|
var blocked = Plan(plate);
|
|
Assert.Equal(new[] { 70, 80.5 + Tolerance.Epsilon }, Positions(blocked));
|
|
Assert.True(blocked.HasSeparatedTail);
|
|
plate.ClampZones.Clear();
|
|
Assert.Equal(new[] { 35, 70, 80.5 + Tolerance.Epsilon }, Positions(Plan(plate)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExistingLineAcrossClampCannotCertifyTail_SeparatorMovesPastClamp()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.ClampZones.Add(new ClampZone("hanging", new Box(80.6, 0, 0.2, 0.625)));
|
|
var existing = new CutOff(new Vector(80.5 + Tolerance.Epsilon, 0), CutOffAxis.Vertical);
|
|
plate.CutOffs.Add(existing);
|
|
existing.Regenerate(plate, new CutOffSettings());
|
|
var original = existing.Drawing.Program;
|
|
var plan = Plan(plate);
|
|
// The existing line is not certified; a new separator past the clamp keeps the tail.
|
|
Assert.Equal(81.05 + Tolerance.Epsilon, plan.TailSeparatorX!.Value, 9);
|
|
Assert.Equal(plan.TailSeparatorX, plan.Definitions[^1].Position.X);
|
|
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff);
|
|
Assert.Same(original, existing.Drawing.Program);
|
|
Assert.Same(existing, Assert.Single(plate.CutOffs));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void NearEquivalentExistingSeparatorInsideClampCannotCertifyTail(int quadrant)
|
|
{
|
|
// The nominal separator (80.50001) lies just beyond the clamp edge (80.5000075),
|
|
// but the equivalent existing line that would be certified (80.500005) is inside it.
|
|
var plate = MakePlate(quadrant);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
// Footprint 79.500005..80.5000075 from the origin along the edge, in every quadrant.
|
|
plate.SetClampRail(new ClampRail(1.0000025, 0.625, new[] { 80.00000625 }));
|
|
var existing = new CutOff(new Vector(sign * 80.500005, 0), CutOffAxis.Vertical);
|
|
plate.CutOffs.Add(existing);
|
|
existing.Regenerate(plate, new CutOffSettings());
|
|
var original = existing.Drawing.Program;
|
|
|
|
var plan = Plan(plate);
|
|
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Null(plan.TailSeparatorX);
|
|
Assert.Equal(0, plan.TailLength);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampBlockedCut);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
|
|
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff);
|
|
Assert.Equal(new[] { sign * 35.0, sign * 70.0 }, Positions(plan));
|
|
Assert.Same(original, existing.Drawing.Program);
|
|
Assert.Same(existing, Assert.Single(plate.CutOffs));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void RailClampsBlockGridAndMoveTailInEveryQuadrant(int quadrant)
|
|
{
|
|
// Rail clamps centered 35 and 80.5 from the origin, 1.5 wide, plus 0.25 clearance:
|
|
// the grid line at 35 is skipped and the separator at 80.50001 moves past the
|
|
// clamp's clearance (79.75..81.25 + 0.25) to 81.50001.
|
|
var plate = MakePlate(quadrant);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 35.0, 80.5 }));
|
|
|
|
var plan = Plan(plate);
|
|
|
|
AssertPositions(new[] { sign * 70.0, sign * (81.5 + Tolerance.Epsilon) }, plan);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.Single(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampBlockedCut);
|
|
var moved = Assert.Single(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampMovedTailSeparator);
|
|
Assert.Equal(plan.TailSeparatorX, moved.X);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void TailSeparatorStepsPastAdjacentClamps(int quadrant)
|
|
{
|
|
// Moving past the first clamp (79.75..81.25) lands in the second one's clearance
|
|
// (81.6..83.1 less 0.25), so the separator steps past both to 83.35001.
|
|
var plate = MakePlate(quadrant);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 80.5, 82.35 }));
|
|
|
|
var plan = Plan(plate);
|
|
|
|
Assert.Equal(sign * (83.35 + Tolerance.Epsilon), plan.TailSeparatorX!.Value, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClampMovedTailBelowMinimumKeepsNoTail()
|
|
{
|
|
// Past the clamp the tail is 120 - 81.50001 = 38.49999, under a 38.6 minimum.
|
|
var plate = MakePlate();
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 80.5 }));
|
|
|
|
var plan = AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 38.6 }, new CutOffSettings());
|
|
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.TailBelowMinimum);
|
|
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampMovedTailSeparator);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void RailClampClearanceOnlyContactBlocksAndClearLineSurvives(int quadrant)
|
|
{
|
|
// Physical footprint 36..37.5; the 0.25 clearance reaches a line at 35.75 but not 35.
|
|
var plate = MakePlate(quadrant);
|
|
var sign = quadrant is 2 or 3 ? -1 : 1;
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 36.75 }));
|
|
Assert.Equal(new[] { sign * 35.0, sign * 70.0, sign * (80.5 + Tolerance.Epsilon) },
|
|
Positions(Plan(plate)));
|
|
Assert.Equal(new[] { sign * 71.5, sign * (80.5 + Tolerance.Epsilon) },
|
|
Positions(Plan(plate, spacing: 35.75)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void HangingRailClampKeepsWholeFootprintForTail(int quadrant)
|
|
{
|
|
// Clamp centered 120.5 on a 120-long sheet covers 119.75..121.25 from the origin;
|
|
// with 0.25 clearance it blocks the separator at 119.50001 behind a 119-long envelope.
|
|
var plate = MakePlate(quadrant, occupied: 119);
|
|
plate.EdgeSpacing = new Spacing(0.25, 0.25);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 120.5 }));
|
|
var plan = Plan(plate, spacing: 119.6);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampBlockedCut);
|
|
Assert.Empty(plan.Definitions);
|
|
}
|
|
|
|
[Fact]
|
|
public void SameLineLimitedManualCutOnClamp_StillBlocksWholePlan()
|
|
{
|
|
const double x = 35;
|
|
var plate = MakePlate();
|
|
plate.ClampZones.Add(new ClampZone("under-manual", new Box(x - 0.75, 0, 1.5, 0.625)));
|
|
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical) { StartLimit = 10, EndLimit = 60 };
|
|
plate.CutOffs.Add(manual);
|
|
var program = manual.Drawing.Program;
|
|
|
|
var plan = Plan(plate);
|
|
|
|
Assert.True(plan.HasBlockingDiagnostics);
|
|
Assert.Empty(plan.Definitions);
|
|
Assert.Empty(plan.PreviewParts);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.IsBlocking &&
|
|
d.Code == AutomaticCutOffDiagnosticCode.LimitedCutOffConflict);
|
|
Assert.Same(program, manual.Drawing.Program);
|
|
Assert.Same(manual, Assert.Single(plate.CutOffs));
|
|
}
|
|
|
|
[Fact]
|
|
public void Planning_DoesNotMutateAnyLiveObjectsOrSequence()
|
|
{
|
|
var plate = MakePlate();
|
|
var drawing = plate.Parts[0].BaseDrawing;
|
|
var nest = new Nest("test");
|
|
nest.Drawings.Add(drawing);
|
|
nest.Plates.Add(plate);
|
|
var manual = new CutOff(new Vector(25, 0), CutOffAxis.Vertical)
|
|
{ StartLimit = 5, EndLimit = 12 };
|
|
plate.CutOffs.Add(manual);
|
|
plate.RegenerateCutOffs(new CutOffSettings());
|
|
var cutoffPart = plate.Parts[^1];
|
|
plate.Parts.Remove(cutoffPart);
|
|
plate.Parts.Insert(0, cutoffPart);
|
|
var parts = plate.Parts.ToArray();
|
|
var definitions = plate.CutOffs.ToArray();
|
|
var programs = parts.Select(p => p.Program).ToArray();
|
|
var text = programs.Select(p => p.ToString()).ToArray();
|
|
var locations = parts.Select(p => p.Location).ToArray();
|
|
var bounds = parts.Select(p => p.BoundingBox).ToArray();
|
|
var drawingProgram = drawing.Program;
|
|
var cutoffProgram = manual.Drawing.Program;
|
|
var nested = drawing.Quantity.Nested;
|
|
var changes = 0;
|
|
plate.PartAdded += (_, _) => changes++;
|
|
plate.PartRemoved += (_, _) => changes++;
|
|
plate.PartChanged += (_, _) => changes++;
|
|
|
|
var plan = Plan(plate);
|
|
|
|
Assert.NotEmpty(plan.Definitions);
|
|
Assert.Equal(parts, plate.Parts.ToArray());
|
|
Assert.Equal(definitions, plate.CutOffs.ToArray());
|
|
Assert.Equal(programs, plate.Parts.Select(p => p.Program));
|
|
Assert.Equal(text, plate.Parts.Select(p => p.Program.ToString()));
|
|
Assert.Equal(locations, plate.Parts.Select(p => p.Location));
|
|
Assert.Equal(bounds, plate.Parts.Select(p => p.BoundingBox));
|
|
Assert.Same(drawingProgram, drawing.Program);
|
|
Assert.Same(cutoffProgram, manual.Drawing.Program);
|
|
Assert.Equal(nested, drawing.Quantity.Nested);
|
|
Assert.Same(drawing, Assert.Single(nest.Drawings));
|
|
Assert.Equal(5, manual.StartLimit);
|
|
Assert.Equal(12, manual.EndLimit);
|
|
Assert.Equal(0, changes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(CutDirection.AwayFromOrigin)]
|
|
[InlineData(CutDirection.TowardOrigin)]
|
|
public void RepeatedLines_UseCurrentManualSegmentation_WithoutIncreasingClearance(CutDirection direction)
|
|
{
|
|
var plate = MakePlate();
|
|
plate.PartSpacing = 8;
|
|
var settings = new CutOffSettings
|
|
{ PartClearance = 1, Overtravel = 2, MinSegmentLength = 0.5, CutDirection = direction };
|
|
var plan = Plan(plate, settings: settings);
|
|
var automatic = plan.Definitions[0];
|
|
var manual = new CutOff(automatic.Position, automatic.Axis);
|
|
manual.Regenerate(plate, settings, Plate.BuildPerimeterCache(plate));
|
|
Assert.Equal(manual.Drawing.Program.ToString(), automatic.Drawing.Program.ToString());
|
|
var segments = Segments(automatic.Drawing.Program);
|
|
Assert.Equal(2, segments.Length);
|
|
Assert.All(segments, s => Assert.True(
|
|
System.Math.Max(s.From.Y, s.To.Y) <= 19 ||
|
|
System.Math.Min(s.From.Y, s.To.Y) >= 41));
|
|
Assert.Contains(segments, s => System.Math.Abs(s.To.Y - 19) < 0.01 ||
|
|
System.Math.Abs(s.From.Y - 19) < 0.01);
|
|
Assert.Equal(1, settings.PartClearance);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPlate_AndCutOffOnlyPlate_AreNeverChopped()
|
|
{
|
|
var plate = new Plate(81, 120);
|
|
Assert.Empty(Plan(plate).Definitions);
|
|
var old = new CutOff(new Vector(110, 0), CutOffAxis.Vertical);
|
|
plate.CutOffs.Add(old);
|
|
plate.RegenerateCutOffs(new CutOffSettings());
|
|
var plan = Plan(plate);
|
|
Assert.Empty(plan.Definitions);
|
|
Assert.Empty(plan.PreviewParts);
|
|
Assert.Equal(0, plan.OccupiedSpan);
|
|
Assert.Equal(0, plan.UsedSpan);
|
|
Assert.Equal(0, plan.TailLength);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
}
|
|
|
|
[Fact]
|
|
public void OldCutOffParts_DoNotExtendTheEnvelope()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.CutOffs.Add(new CutOff(new Vector(115, 0), CutOffAxis.Vertical));
|
|
plate.RegenerateCutOffs(new CutOffSettings());
|
|
Assert.Equal(new[] { 35, 70, 80.5 + Tolerance.Epsilon }, Positions(Plan(plate)));
|
|
}
|
|
|
|
[Fact]
|
|
public void UsedSpanSmallerThanPitch_OnlyGeneratesSeparator()
|
|
{
|
|
var plan = Plan(MakePlate(occupied: 20));
|
|
Assert.Equal(new[] { 20.5 + Tolerance.Epsilon }, Positions(plan));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExactPitchAtSeparator_DoesNotGenerateBoundaryTwice()
|
|
{
|
|
var plate = MakePlate(occupied: 69.5 - Tolerance.Epsilon);
|
|
Assert.Equal(new[] { 35.0, 70.0 }, Positions(Plan(plate)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(119.5)]
|
|
[InlineData(120)]
|
|
public void NoRoomForSeparator_UsesFullSheet_NoEdgeDuplicate(double occupied)
|
|
{
|
|
var plan = Plan(MakePlate(occupied: occupied), spacing: 40);
|
|
Assert.Equal(new[] { 40.0, 80.0 }, Positions(plan));
|
|
Assert.Equal(120, plan.UsedSpan);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Null(plan.TailSeparatorX);
|
|
Assert.Equal(0, plan.TailLength);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-1)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
[InlineData(double.Epsilon)]
|
|
[InlineData(0.000001)]
|
|
[InlineData(0.00001)]
|
|
[InlineData(0.001)]
|
|
public void InvalidOrExcessivePitch_IsRejected(double pitch)
|
|
{
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(MakePlate(), pitch));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-1)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
public void InvalidSheetDimensions_AreRejected(double value)
|
|
{
|
|
var plate = MakePlate();
|
|
plate.Size = new Size(value, 120);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
plate.Size = new Size(81, value);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
public void InvalidPartSpacingAndSettings_AreRejected(double value)
|
|
{
|
|
var plate = MakePlate();
|
|
plate.PartSpacing = value;
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
plate.PartSpacing = 0;
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate,
|
|
settings: new CutOffSettings { PartClearance = value }));
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate,
|
|
settings: new CutOffSettings { Overtravel = value }));
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate,
|
|
settings: new CutOffSettings { MinSegmentLength = value }));
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidQuadrantOrDirection_IsRejected()
|
|
{
|
|
var plate = MakePlate();
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate,
|
|
settings: new CutOffSettings { CutDirection = (CutDirection)42 }));
|
|
// The public setter coerces bad quadrants; simulate damaged state explicitly.
|
|
typeof(Plate).GetField("quadrant", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.SetValue(plate, 0);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
[Fact]
|
|
public void NullArguments_AreRejected()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => AutomaticCutOffPlanner.Create(null!, new(), new()));
|
|
Assert.Throws<ArgumentNullException>(() => AutomaticCutOffPlanner.Create(MakePlate(), null!, new()));
|
|
Assert.Throws<ArgumentNullException>(() => AutomaticCutOffPlanner.Create(MakePlate(), new(), null!));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-11, 0)]
|
|
[InlineData(50, 0)]
|
|
[InlineData(0, -21)]
|
|
[InlineData(0, 42)]
|
|
[InlineData(double.NaN, 0)]
|
|
[InlineData(double.PositiveInfinity, 0)]
|
|
public void InvalidOrOutOfSheetGeometry_IsRejected(double dx, double dy)
|
|
{
|
|
var plate = MakePlate();
|
|
plate.Parts[0].Offset(dx, dy);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyOrNonfiniteProgramGeometry_IsRejected_EvenWithOldBounds()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.Parts[0].Program.Codes.Clear();
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
plate = MakePlate();
|
|
((Motion)plate.Parts[0].Program.Codes[2]).EndPoint = new Vector(double.NaN, 2);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyRepeatedCuts_AreDiagnostic_NotAcceptedPartitions()
|
|
{
|
|
var plate = new Plate(81, 120);
|
|
plate.Parts.Add(Rectangle(80, 81, new Vector()));
|
|
var plan = Plan(plate);
|
|
Assert.Single(plan.Definitions);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.Equal(2, plan.Diagnostics.Count(d => d.Code == AutomaticCutOffDiagnosticCode.EmptyCut));
|
|
}
|
|
|
|
[Fact]
|
|
public void FilteredSeparator_DoesNotClaimSeparatedTail()
|
|
{
|
|
var plan = Plan(MakePlate(), settings: new CutOffSettings { MinSegmentLength = 100 });
|
|
Assert.Empty(plan.Definitions);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Equal(0, plan.TailLength);
|
|
Assert.Null(plan.TailSeparatorX);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyingDefinitions_ThenRerunning_PreservesPartsProgramsQuantitiesAndSequence()
|
|
{
|
|
var plate = MakePlate();
|
|
var real = plate.Parts[0];
|
|
var secondReal = new Part(real.BaseDrawing, new Vector(0, 60));
|
|
plate.Parts.Add(secondReal);
|
|
var secondProgram = secondReal.Program;
|
|
var secondLocation = secondReal.Location;
|
|
var program = real.Program;
|
|
var text = program.ToString();
|
|
var location = real.Location;
|
|
var nested = real.BaseDrawing.Quantity.Nested;
|
|
var settings = new CutOffSettings { Overtravel = 2, PartClearance = 0.75 };
|
|
var manual = new CutOff(new Vector(15, 0), CutOffAxis.Horizontal)
|
|
{ StartLimit = 5, EndLimit = 15 };
|
|
plate.CutOffs.Add(manual);
|
|
plate.RegenerateCutOffs(settings);
|
|
var manualPart = plate.Parts[^1];
|
|
plate.Parts.Remove(manualPart);
|
|
plate.Parts.Insert(0, manualPart);
|
|
var first = Plan(plate, settings: settings);
|
|
foreach (var definition in first.Definitions)
|
|
plate.CutOffs.Add(definition);
|
|
plate.RegenerateCutOffs(settings);
|
|
var second = Plan(plate, settings: settings);
|
|
Assert.Empty(second.Definitions);
|
|
Assert.True(second.HasSeparatedTail);
|
|
Assert.Equal(first.UsedSpan, second.UsedSpan);
|
|
Assert.Equal(3, second.Diagnostics.Count(d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff));
|
|
plate.RegenerateCutOffs(settings);
|
|
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
|
Assert.All(plate.CutOffs, c => Assert.Equal(c.SegmentPrograms().Count, TestHelpers.CutOffParts(plate, c).Length));
|
|
Assert.Same(manual.Drawing, plate.Parts[0].BaseDrawing);
|
|
Assert.Same(real, plate.Parts[1]);
|
|
Assert.Same(secondReal, plate.Parts[2]);
|
|
Assert.Same(secondProgram, secondReal.Program);
|
|
Assert.Equal(secondLocation, secondReal.Location);
|
|
Assert.Same(program, real.Program);
|
|
Assert.Equal(text, real.Program.ToString());
|
|
Assert.Equal(location, real.Location);
|
|
Assert.Equal(nested, real.BaseDrawing.Quantity.Nested);
|
|
Assert.Equal(5, manual.StartLimit);
|
|
Assert.Equal(15, manual.EndLimit);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void EquivalentFullSpanLimits_AndCoordinateTolerance_SuppressDuplicates(bool explicitLimits)
|
|
{
|
|
var plate = MakePlate();
|
|
var existing = new CutOff(new Vector(35 + Tolerance.Epsilon / 2, 999), CutOffAxis.Vertical);
|
|
if (explicitLimits)
|
|
{
|
|
existing.StartLimit = 0;
|
|
existing.EndLimit = 83;
|
|
}
|
|
plate.CutOffs.Add(existing);
|
|
var plan = Plan(plate, settings: new CutOffSettings { Overtravel = 2 });
|
|
Assert.Equal(new[] { 70, 80.5 + Tolerance.Epsilon }, Positions(plan));
|
|
Assert.False(plan.HasBlockingDiagnostics);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff);
|
|
}
|
|
|
|
[Fact]
|
|
public void DifferentAxisOrDifferentCoordinate_DoesNotSuppress()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.CutOffs.Add(new CutOff(new Vector(35, 35), CutOffAxis.Horizontal));
|
|
plate.CutOffs.Add(new CutOff(new Vector(35.01, 0), CutOffAxis.Vertical));
|
|
Assert.Equal(3, Plan(plate).Definitions.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(35)]
|
|
[InlineData(80.50001)]
|
|
public void SameLineLimitedManualCut_BlocksWholePlan_WithoutMutatingManual(double x)
|
|
{
|
|
var plate = MakePlate();
|
|
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical)
|
|
{ StartLimit = 10, EndLimit = 60 };
|
|
plate.CutOffs.Add(manual);
|
|
var program = manual.Drawing.Program;
|
|
var plan = Plan(plate);
|
|
Assert.True(plan.HasBlockingDiagnostics);
|
|
Assert.Empty(plan.Definitions);
|
|
Assert.Empty(plan.PreviewParts);
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.IsBlocking &&
|
|
d.Code == AutomaticCutOffDiagnosticCode.LimitedCutOffConflict);
|
|
Assert.Same(program, manual.Drawing.Program);
|
|
Assert.Equal(10, manual.StartLimit);
|
|
Assert.Equal(60, manual.EndLimit);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExistingFullSpanAlongsideLimitedConflict_DoesNotHideConflict()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.CutOffs.Add(new CutOff(new Vector(35, 0), CutOffAxis.Vertical));
|
|
plate.CutOffs.Add(new CutOff(new Vector(35, 0), CutOffAxis.Vertical) { EndLimit = 40 });
|
|
Assert.True(Plan(plate).HasBlockingDiagnostics);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExistingSeparator_MetadataUsesItsActualToleranceCloseCoordinate()
|
|
{
|
|
var plate = MakePlate();
|
|
var x = 80.5 + Tolerance.Epsilon * 1.5;
|
|
plate.CutOffs.Add(new CutOff(new Vector(x, 0), CutOffAxis.Vertical));
|
|
var plan = Plan(plate);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.Equal(x, plan.TailSeparatorX);
|
|
Assert.Equal(x, plan.UsedSpan);
|
|
Assert.Equal(120 - x, plan.TailLength);
|
|
}
|
|
|
|
[Fact]
|
|
public void NearEquivalentExistingSeparator_ThatStillTouchesClearance_DoesNotClaimTail()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.PartSpacing = 0;
|
|
plate.CutOffs.Add(new CutOff(new Vector(80, 0), CutOffAxis.Vertical));
|
|
var plan = Plan(plate, settings: new CutOffSettings { PartClearance = 0 });
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Equal(0, plan.TailLength);
|
|
Assert.DoesNotContain(plan.Definitions, c => c.Position.X > 70);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
|
|
}
|
|
|
|
[Fact]
|
|
public void FractionalPitch_UsesIntegerMultiplicationWithoutCumulativeDrift()
|
|
{
|
|
var plate = MakePlate(occupied: 11);
|
|
var plan = Plan(plate, spacing: 0.7);
|
|
var repeated = plan.Definitions.Take(plan.Definitions.Count - 1).ToArray();
|
|
Assert.NotEmpty(repeated);
|
|
for (var i = 0; i < repeated.Length; i++)
|
|
Assert.Equal((i + 1) * 0.7, repeated[i].Position.X);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcaveOrSlopedPart_UsesPerimeterNotJustItsBoundingBox()
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(30, 0));
|
|
program.Codes.Add(new LinearMove(0, 30));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var plate = new Plate(81, 120);
|
|
plate.Parts.Add(new Part(new Drawing("triangle", program), new Vector(10, 10)));
|
|
var plan = Plan(plate, spacing: 20, settings: new CutOffSettings { PartClearance = 0 });
|
|
var segments = Segments(plan.Definitions[0].Drawing.Program);
|
|
Assert.Equal(2, segments.Length);
|
|
Assert.Equal(30, segments[1].From.Y, 8); // Bounding-box fallback would start at 40.
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.SegmentedCut);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidButStalePartBounds_AreRejectedRatherThanUsingUnsafeBroadphase()
|
|
{
|
|
var plate = MakePlate();
|
|
((Motion)plate.Parts[0].Program.Codes[2]).EndPoint = new Vector(90, 20);
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
private static Plate MakeArcPlate(double centerError)
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(10, 0));
|
|
program.Codes.Add(new ArcMove(10, 20, 10, 10 + centerError, RotationType.CCW));
|
|
program.Codes.Add(new LinearMove(0, 20));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var material = ConvertProgram.ToGeometry(program).Where(e => SpecialLayers.IsMaterial(e.Layer));
|
|
Assert.True(Assert.Single(ShapeBuilder.GetShapes(material)).IsClosed());
|
|
var drawing = new Drawing("D shape", program);
|
|
Assert.Equal(200 + 50 * System.Math.PI, drawing.Area, 2); // Drawing area tessellates arcs.
|
|
var plate = new Plate(81, 120) { PartSpacing = 0 };
|
|
plate.Parts.Add(new Part(drawing, new Vector(10, 10)));
|
|
return plate;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.0001, false)]
|
|
[InlineData(0.0000075, true)]
|
|
public void ConvertedArcOutsideCachedBounds_IsRejectedBeforeAcceptance(double centerError, bool repeatedCut)
|
|
{
|
|
var plate = MakeArcPlate(centerError);
|
|
var real = plate.Parts[0];
|
|
var cached = real.BoundingBox;
|
|
var raw = real.Program.BoundingBox().Translate(real.Location);
|
|
Assert.Equal(raw.Right, cached.Right); // Neither cache refresh nor raw-program bounds reveal the error.
|
|
Assert.Equal(30 - centerError, cached.Right, 10);
|
|
var arc = Assert.Single(ConvertProgram.ToGeometry(real.Program).OfType<Arc>());
|
|
arc.Offset(real.Location);
|
|
Assert.Equal(30, arc.BoundingBox.Right);
|
|
var protrusion = arc.BoundingBox.Right - cached.Right;
|
|
Assert.True(protrusion > 0);
|
|
Assert.Equal(repeatedCut, protrusion < AutomaticCutOffPlanner.GeometryTolerance);
|
|
|
|
// A separator crosses the larger protrusion. Even sub-tolerance protrusions are
|
|
// unsafe when a repeated line falls between the cached and converted bounds.
|
|
var unsafeX = cached.Right + (repeatedCut ? protrusion / 2 : AutomaticCutOffPlanner.GeometryTolerance);
|
|
var unsafeLine = new Line(new Vector(unsafeX, 0), new Vector(unsafeX, 81));
|
|
Assert.True(arc.Intersects(unsafeLine, out var intersections));
|
|
Assert.Equal(2, intersections.Count);
|
|
if (repeatedCut)
|
|
plate.Parts.Add(Rectangle(10, 10, new Vector(60, 50)));
|
|
var settings = new CutOffSettings { PartClearance = 0 };
|
|
var manual = new CutOff(new Vector(100, 0), CutOffAxis.Vertical);
|
|
plate.CutOffs.Add(manual);
|
|
plate.RegenerateCutOffs(settings);
|
|
var parts = plate.Parts.ToArray();
|
|
var definitions = plate.CutOffs.ToArray();
|
|
var programs = parts.Select(p => p.Program).ToArray();
|
|
var text = programs.Select(p => p.ToString()).ToArray();
|
|
var locations = parts.Select(p => p.Location).ToArray();
|
|
var bounds = parts.Select(p => p.BoundingBox).ToArray();
|
|
var manualProgram = manual.Drawing.Program;
|
|
var originalCenter = Assert.IsType<ArcMove>(real.Program.Codes[2]).CenterPoint;
|
|
var changes = 0;
|
|
plate.PartAdded += (_, _) => changes++;
|
|
plate.PartRemoved += (_, _) => changes++;
|
|
plate.PartChanged += (_, _) => changes++;
|
|
AutomaticCutOffPlan? plan = null;
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
plan = Plan(plate, spacing: repeatedCut ? unsafeX : 35, settings: settings);
|
|
foreach (var definition in plan.Definitions)
|
|
plate.CutOffs.Add(definition);
|
|
plate.RegenerateCutOffs(settings);
|
|
});
|
|
|
|
Assert.Null(plan); // No unsafe proposal can escape to the normal acceptance path.
|
|
Assert.Equal(parts, plate.Parts.ToArray());
|
|
Assert.Equal(definitions, plate.CutOffs.ToArray());
|
|
Assert.Equal(programs, plate.Parts.Select(p => p.Program));
|
|
Assert.Equal(text, plate.Parts.Select(p => p.Program.ToString()));
|
|
Assert.Equal(locations, plate.Parts.Select(p => p.Location));
|
|
Assert.Equal(bounds, plate.Parts.Select(p => p.BoundingBox));
|
|
Assert.Same(manualProgram, manual.Drawing.Program);
|
|
Assert.Equal(originalCenter.X, Assert.IsType<ArcMove>(real.Program.Codes[2]).CenterPoint.X);
|
|
Assert.Equal(originalCenter.Y, Assert.IsType<ArcMove>(real.Program.Codes[2]).CenterPoint.Y);
|
|
Assert.Equal(0, changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void LargeArcRefit_DoesNotReplaceConvertedPerimeterWithUndersizedBox()
|
|
{
|
|
var plate = MakeArcPlate(0.0001);
|
|
var part = Assert.Single(plate.Parts);
|
|
var perimeter = Plate.BuildPerimeterCache(plate)[part];
|
|
Assert.NotNull(perimeter);
|
|
Assert.True(perimeter.BoundingBox.Right > part.BoundingBox.Right + CutOff.GetBoundsRoundoff(part));
|
|
var arc = Assert.Single(ConvertProgram.ToGeometry(part.Program).OfType<Arc>());
|
|
arc.Offset(part.Location);
|
|
var x = (part.BoundingBox.Right + arc.BoundingBox.Right) / 2;
|
|
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical);
|
|
manual.Regenerate(plate, new CutOffSettings { PartClearance = 0 }, Plate.BuildPerimeterCache(plate));
|
|
foreach (var segment in Segments(manual.Drawing.Program))
|
|
Assert.False(arc.Intersects(new Line(segment.From, segment.To), out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LargeArcRefit_UncachedDirectRegenerate_DoesNotCrossConvertedPerimeter()
|
|
{
|
|
// Same large-refit scenario as above, but the caller never built a
|
|
// perimeter cache at all (OpenNest PM 13f7a685: a direct/uncached
|
|
// CutOff.Regenerate call used to trust the bare BoundingBox and could
|
|
// emit a segment crossing the arc's true converted extent).
|
|
var plate = MakeArcPlate(0.0001);
|
|
var part = Assert.Single(plate.Parts);
|
|
var cachedBox = part.BoundingBox;
|
|
var arc = Assert.Single(ConvertProgram.ToGeometry(part.Program).OfType<Arc>());
|
|
arc.Offset(part.Location);
|
|
var x = (cachedBox.Right + arc.BoundingBox.Right) / 2;
|
|
Assert.True(x > cachedBox.Right); // The unsafe line sits outside the cached bounds.
|
|
|
|
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical);
|
|
manual.Regenerate(plate, new CutOffSettings { PartClearance = 0 }); // No cache argument.
|
|
|
|
foreach (var segment in Segments(manual.Drawing.Program))
|
|
Assert.False(arc.Intersects(new Line(segment.From, segment.To), out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExactArcWithinCachedBounds_PlansAndMaterializesSafeCuts()
|
|
{
|
|
var plate = MakeArcPlate(0);
|
|
var real = plate.Parts[0];
|
|
var program = real.Program;
|
|
var settings = new CutOffSettings { PartClearance = 0 };
|
|
var plan = Plan(plate, spacing: 25, settings: settings);
|
|
Assert.True(plan.HasSeparatedTail);
|
|
Assert.False(plan.HasBlockingDiagnostics);
|
|
Assert.Equal(30, plan.OccupiedSpan);
|
|
Assert.Equal(new[] { 25, 30 + AutomaticCutOffPlanner.GeometryTolerance }, Positions(plan));
|
|
Assert.Same(real, Assert.Single(plate.Parts));
|
|
Assert.Empty(plate.CutOffs);
|
|
var previewSegments = plan.PreviewParts.Select(p => Segments(p.Program)).ToArray();
|
|
|
|
foreach (var definition in plan.Definitions)
|
|
plate.CutOffs.Add(definition);
|
|
plate.RegenerateCutOffs(settings);
|
|
|
|
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
|
for (var i = 0; i < plan.Definitions.Count; i++)
|
|
Assert.Equal(previewSegments[i], Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[i])));
|
|
var repeated = Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[0]));
|
|
Assert.Equal(2, repeated.Length);
|
|
Assert.Equal(20 - System.Math.Sqrt(75), repeated[0].To.Y, 8);
|
|
Assert.Equal(20 + System.Math.Sqrt(75), repeated[1].From.Y, 8);
|
|
var separator = Assert.Single(Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[1])));
|
|
Assert.Equal(new Vector(plan.TailSeparatorX!.Value, 0), separator.From);
|
|
Assert.Equal(new Vector(plan.TailSeparatorX.Value, 81), separator.To);
|
|
var arc = Assert.Single(ConvertProgram.ToGeometry(real.Program).OfType<Arc>());
|
|
arc.Offset(real.Location);
|
|
Assert.False(arc.Intersects(new Line(separator.From, separator.To), out _));
|
|
Assert.Same(real, plate.Parts[0]);
|
|
Assert.Same(program, real.Program);
|
|
var repeatedPlan = Plan(plate, spacing: 25, settings: settings);
|
|
Assert.Empty(repeatedPlan.Definitions);
|
|
Assert.True(repeatedPlan.HasSeparatedTail);
|
|
Assert.Equal(plan.TailSeparatorX, repeatedPlan.TailSeparatorX);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidArcCenterAndRecursiveProgram_AreRejectedBeforeGeometryConversion()
|
|
{
|
|
var plate = MakePlate();
|
|
plate.Parts[0].Program.Codes.Add(new ArcMove(new Vector(), new Vector(double.NaN, 0)));
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
plate = MakePlate();
|
|
var program = plate.Parts[0].Program;
|
|
program.Codes.Add(new SubProgramCall { Program = program });
|
|
Assert.ThrowsAny<ArgumentException>(() => Plan(plate));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void ApplyAndRepeat_IsIdempotentInEveryQuadrant(int quadrant)
|
|
{
|
|
var plate = MakePlate(quadrant);
|
|
var settings = new CutOffSettings();
|
|
var first = Plan(plate, settings: settings);
|
|
foreach (var definition in first.Definitions)
|
|
plate.CutOffs.Add(definition);
|
|
plate.RegenerateCutOffs(settings);
|
|
var second = Plan(plate, settings: settings);
|
|
Assert.Empty(second.Definitions);
|
|
Assert.True(second.HasSeparatedTail);
|
|
Assert.Equal(first.TailSeparatorX, second.TailSeparatorX);
|
|
Assert.Equal(first.TailLength, second.TailLength);
|
|
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExistingSeparator_IsVerifiedUnderCurrentSettings_NotStaleProgram()
|
|
{
|
|
var plate = MakePlate();
|
|
var existing = new CutOff(new Vector(80.5 + Tolerance.Epsilon, 0), CutOffAxis.Vertical);
|
|
existing.Regenerate(plate, new CutOffSettings());
|
|
Assert.NotEmpty(existing.Drawing.Program.Codes);
|
|
plate.CutOffs.Add(existing);
|
|
var program = existing.Drawing.Program;
|
|
var plan = Plan(plate, settings: new CutOffSettings { MinSegmentLength = 100 });
|
|
Assert.False(plan.HasSeparatedTail);
|
|
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
|
|
Assert.Same(program, existing.Drawing.Program);
|
|
}
|
|
}
|