mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-17 01:41:36 -04:00
Plate.RemovedClamps exposed a HashSet (and every untouched plate shared one static empty HashSet), so a downcast could change one plate's removal state or every plate's at once without re-resolving zones. It is now an ImmutableHashSet. PM 6ded8ac4-84fd-4e8b-9a3b-6f41e494542c item (e)
399 lines
15 KiB
C#
399 lines
15 KiB
C#
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests;
|
|
|
|
/// <summary>
|
|
/// The fixed machine clamp rail: X centers measured from the plate origin along the
|
|
/// origin-side edge, resolved onto every sheet size without re-scaling (D2/D3).
|
|
/// </summary>
|
|
public class ClampRailTests
|
|
{
|
|
// The user's machine: ~8 clamps, 1.5" along the edge x 0.625" into the sheet.
|
|
private static readonly double[] MachineCenters = { 6, 21, 36, 51, 66, 81, 96, 111 };
|
|
|
|
private static ClampRail Machine() => new(1.5, 0.625, MachineCenters);
|
|
|
|
// Plate(width, length): Width is the Y extent, Length the X extent along the clamp edge.
|
|
private static Plate Sheet(double width, double length, int quadrant = 1) =>
|
|
new(width, length) { Quadrant = quadrant };
|
|
|
|
[Theory]
|
|
[InlineData(0, 0.625)]
|
|
[InlineData(-1, 0.625)]
|
|
[InlineData(double.NaN, 0.625)]
|
|
[InlineData(double.PositiveInfinity, 0.625)]
|
|
[InlineData(1.5, 0)]
|
|
[InlineData(1.5, -0.5)]
|
|
[InlineData(1.5, double.NaN)]
|
|
public void NonPositiveOrNonFiniteSizesAreRejected(double width, double depth)
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new ClampRail(width, depth, new[] { 6.0 }));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-0.5)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
public void CentersMustBeFiniteAndFromTheOrigin(double center)
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new ClampRail(1.5, 0.625, new[] { 6, center }));
|
|
Assert.Throws<ArgumentNullException>(() => new ClampRail(1.5, 0.625, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void CentersAreSortedAndDistinct()
|
|
{
|
|
var rail = new ClampRail(1.5, 0.625, new[] { 36.0, 6, 21, 6 });
|
|
Assert.Equal(new[] { 6.0, 21, 36 }, rail.Centers);
|
|
}
|
|
|
|
[Fact]
|
|
public void FullLengthSheetGetsEveryClampOnTheFrontEdge()
|
|
{
|
|
var zones = Machine().ResolveFor(new Size(60, 120), quadrant: 1);
|
|
|
|
Assert.Equal(8, zones.Count);
|
|
for (var i = 0; i < zones.Count; i++)
|
|
{
|
|
Assert.Equal($"clamp-{i + 1}", zones[i].Id);
|
|
Assert.Equal(MachineCenters[i] - 0.75, zones[i].Bounds.X);
|
|
Assert.Equal(0, zones[i].Bounds.Y);
|
|
Assert.Equal(1.5, zones[i].Bounds.Length);
|
|
Assert.Equal(0.625, zones[i].Bounds.Width);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ShorterSheetSkipsClampsBeyondItWithoutRescaling()
|
|
{
|
|
// D2: a 60" sheet keeps the clamps at their table positions and drops the rest.
|
|
var zones = Machine().ResolveFor(new Size(48, 60), quadrant: 1);
|
|
|
|
Assert.Equal(new[] { "clamp-1", "clamp-2", "clamp-3", "clamp-4" }, zones.Select(z => z.Id));
|
|
Assert.Equal(new[] { 5.25, 20.25, 35.25, 50.25 }, zones.Select(z => z.Bounds.X));
|
|
}
|
|
|
|
[Fact]
|
|
public void ClampReachingOntoTheSheetIsKeptWhole()
|
|
{
|
|
// A clamp is an avoidance area: if any of it reaches the sheet, all of it applies.
|
|
var rail = new ClampRail(1.5, 0.625, new[] { 59.5, 60.5, 60.75, 61 });
|
|
|
|
var zones = rail.ResolveFor(new Size(48, 60), quadrant: 1);
|
|
|
|
// 59.5 covers [58.75, 60.25] and 60.5 still reaches the last 0.25" [59.75, 61.25];
|
|
// 60.75 only touches the sheet end and 61 lies wholly beyond it.
|
|
Assert.Equal(new[] { "clamp-1", "clamp-2" }, zones.Select(z => z.Id));
|
|
Assert.Equal(new[] { 58.75, 59.75 }, zones.Select(z => z.Bounds.X));
|
|
Assert.All(zones, z => Assert.Equal(1.5, z.Bounds.Length));
|
|
}
|
|
|
|
[Fact]
|
|
public void ClampsNearTheOriginOrDeeperThanTheSheetStayWhole()
|
|
{
|
|
var rail = new ClampRail(1.5, 0.625, new[] { 0.25 });
|
|
var origin = Assert.Single(rail.ResolveFor(new Size(48, 96), quadrant: 1));
|
|
Assert.Equal(-0.5, origin.Bounds.X);
|
|
Assert.Equal(1.5, origin.Bounds.Length);
|
|
|
|
var deep = new ClampRail(1.5, 10, new[] { 6.0 });
|
|
var front = Assert.Single(deep.ResolveFor(new Size(4, 96), 1));
|
|
Assert.Equal(0, front.Bounds.Y);
|
|
Assert.Equal(10, front.Bounds.Width);
|
|
// Sheets extending to -Y have the origin edge on top; the clamp hangs below the sheet.
|
|
var top = Assert.Single(deep.ResolveFor(new Size(4, 96), 3));
|
|
Assert.Equal(4.0, top.Bounds.Top);
|
|
Assert.Equal(10, top.Bounds.Width);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 96)]
|
|
[InlineData(48, 0)]
|
|
[InlineData(double.NaN, 96)]
|
|
public void DegenerateSheetsGetNoZones(double width, double length)
|
|
{
|
|
Assert.Empty(Machine().ResolveFor(new Size(width, length), quadrant: 1));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void ZonesSitOnTheOriginEdgeMeasuredFromTheOriginInEveryQuadrant(int quadrant)
|
|
{
|
|
var plate = Sheet(48, 96, quadrant);
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, new[] { 6.0 }));
|
|
var zone = Assert.Single(plate.ClampZones);
|
|
|
|
// Plate coordinates start at the sheet's lower-left corner; translating by the
|
|
// sheet's placement corner gives machine coordinates around the origin.
|
|
var sheet = plate.BoundingBox(includeParts: false);
|
|
var machine = zone.Bounds.Translate(sheet.X, sheet.Y);
|
|
var awayFromOrigin = quadrant is 1 or 4 ? 1 : -1;
|
|
var intoSheet = quadrant is 1 or 2 ? 1 : -1;
|
|
|
|
Assert.Equal(6.0, System.Math.Abs(machine.Center.X), 12);
|
|
Assert.Equal(awayFromOrigin, System.Math.Sign(machine.Center.X));
|
|
Assert.Equal(0.0, intoSheet > 0 ? machine.Bottom : machine.Top, 12);
|
|
Assert.Equal(0.625, machine.Width);
|
|
Assert.Equal(1.5, machine.Length, 12);
|
|
Assert.True(zone.Bounds.Left >= 0 && zone.Bounds.Right <= 96 + 1e-9);
|
|
Assert.True(zone.Bounds.Bottom >= 0 && zone.Bounds.Top <= 48 + 1e-9);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlateFollowsItsRailWhenResizedOrRotated()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine());
|
|
Assert.Equal(8, plate.ClampZones.Count);
|
|
Assert.True(plate.ClampRail.Matches(plate));
|
|
|
|
plate.Size = new Size(48, 60);
|
|
Assert.Equal(4, plate.ClampZones.Count);
|
|
|
|
// Rotating swaps the sheet's extents; the machine's clamps stay where they are.
|
|
plate.Rotate90(RotationType.CW);
|
|
Assert.Equal(new Size(60, 48), plate.Size);
|
|
Assert.Equal(new[] { 5.25, 20.25, 35.25 }, plate.ClampZones.Select(z => z.Bounds.X));
|
|
|
|
plate.Quadrant = 2;
|
|
Assert.True(plate.ClampRail.Matches(plate));
|
|
Assert.Equal(48 - 6.75, plate.ClampZones[0].Bounds.X);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnchangedResolutionRaisesNoEvents()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine());
|
|
var original = plate.ClampZones.ToList();
|
|
var changes = 0;
|
|
plate.ClampZonesChanged += (_, _) => changes++;
|
|
|
|
plate.Size = new Size(48, 120);
|
|
plate.Quadrant = 1;
|
|
plate.SetClampRail(Machine());
|
|
|
|
Assert.Equal(0, changes);
|
|
Assert.Equal(original, plate.ClampZones); // same zone instances
|
|
}
|
|
|
|
[Fact]
|
|
public void DetachingTheRailRemovesItsZones()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine());
|
|
|
|
plate.SetClampRail(null!);
|
|
|
|
Assert.Null(plate.ClampRail);
|
|
Assert.Empty(plate.ClampZones);
|
|
plate.Size = new Size(48, 96);
|
|
Assert.Empty(plate.ClampZones);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlatesWithoutARailKeepExplicitZonesThroughResizes()
|
|
{
|
|
var plate = Sheet(48, 96);
|
|
plate.ClampZones.Add(new ClampZone("manual", new Box(5.25, 0, 1.5, 0.625)));
|
|
|
|
plate.Size = new Size(48, 120);
|
|
plate.Quadrant = 3;
|
|
|
|
Assert.Equal("manual", Assert.Single(plate.ClampZones).Id);
|
|
Assert.False(Machine().Matches(plate));
|
|
}
|
|
|
|
[Fact]
|
|
public void NewPlatesFollowTheNestRailAtTheirOwnSize()
|
|
{
|
|
var nest = new Nest();
|
|
nest.PlateDefaults.Size = new Size(48, 60);
|
|
nest.SetClampRail(Machine());
|
|
|
|
var plate = nest.CreatePlate();
|
|
|
|
Assert.Same(nest.PlateDefaults.ClampRail, plate.ClampRail);
|
|
Assert.Equal(4, plate.ClampZones.Count);
|
|
plate.Size = new Size(60, 120);
|
|
Assert.Equal(8, plate.ClampZones.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void NestRailReplacesEveryPlatesZones()
|
|
{
|
|
var nest = new Nest();
|
|
nest.PlateDefaults.Size = new Size(48, 120);
|
|
var a = nest.CreatePlate();
|
|
var b = nest.CreatePlate();
|
|
b.Size = new Size(48, 60);
|
|
b.ClampZones.Add(new ClampZone("manual", new Box(1, 0, 1, 1)));
|
|
|
|
nest.SetClampRail(Machine());
|
|
Assert.Equal(8, a.ClampZones.Count);
|
|
Assert.Equal(4, b.ClampZones.Count);
|
|
Assert.DoesNotContain(b.ClampZones, z => z.Id == "manual");
|
|
|
|
// A rail without clamps is no rail: zones and the template are cleared everywhere.
|
|
nest.SetClampRail(new ClampRail(1.5, 0.625, Array.Empty<double>()));
|
|
Assert.Null(nest.PlateDefaults.ClampRail);
|
|
Assert.All(nest.Plates, p => Assert.Empty(p.ClampZones));
|
|
Assert.Empty(nest.CreatePlate().ClampZones);
|
|
}
|
|
|
|
[Fact]
|
|
public void RailZonesStayOutOfQuantityAndUtilization()
|
|
{
|
|
var drawing = new Drawing("part", new OpenNest.CNC.Program());
|
|
var nest = new Nest();
|
|
nest.Drawings.Add(drawing);
|
|
nest.SetClampRail(Machine());
|
|
var plate = nest.CreatePlate();
|
|
|
|
Assert.Equal(8, plate.ClampZones.Count);
|
|
Assert.Empty(plate.Parts);
|
|
Assert.Equal(0, plate.Utilization());
|
|
nest.UpdateDrawingQuantities();
|
|
Assert.Equal(0, drawing.Quantity.Nested);
|
|
}
|
|
|
|
[Fact]
|
|
public void RailsCompareByValue()
|
|
{
|
|
Assert.Equal(Machine(), new ClampRail(1.5, 0.625, MachineCenters.Reverse()));
|
|
Assert.Equal(Machine().GetHashCode(), new ClampRail(1.5, 0.625, MachineCenters).GetHashCode());
|
|
Assert.NotEqual(Machine(), new ClampRail(1.5, 0.5, MachineCenters));
|
|
Assert.NotEqual(Machine(), new ClampRail(1.5, 0.625, MachineCenters.Take(7)));
|
|
}
|
|
|
|
// D9: minutes to remove one clamp and put it back, a variable setting on the rail.
|
|
|
|
[Fact]
|
|
public void RemovalMinutesDefaultsToThePlannerValue()
|
|
{
|
|
Assert.Equal(5.0, Machine().RemovalMinutes);
|
|
Assert.Equal(5.0, ClampRail.DefaultRemovalMinutes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(double.NaN)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
public void RemovalMinutesMustBeFiniteAndZeroOrMore(double minutes)
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => new ClampRail(1.5, 0.625, new[] { 6.0 }, minutes));
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroRemovalMinutesIsAllowed()
|
|
{
|
|
var rail = new ClampRail(1.5, 0.625, new[] { 6.0 }, removalMinutes: 0);
|
|
Assert.Equal(0, rail.RemovalMinutes);
|
|
}
|
|
|
|
[Fact]
|
|
public void RailsWithDifferentRemovalMinutesAreNotEqual()
|
|
{
|
|
var fiveMinutes = new ClampRail(1.5, 0.625, MachineCenters, 5);
|
|
var tenMinutes = new ClampRail(1.5, 0.625, MachineCenters, 10);
|
|
Assert.NotEqual(fiveMinutes, tenMinutes);
|
|
Assert.NotEqual(fiveMinutes.GetHashCode(), tenMinutes.GetHashCode());
|
|
}
|
|
|
|
// D8/C13: last-resort per-sheet removal of individual rail clamps.
|
|
|
|
[Fact]
|
|
public void RemovedClampsAreSubtractedFromTheRailResolution()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine(), new[] { "clamp-3" });
|
|
|
|
Assert.Equal(7, plate.ClampZones.Count);
|
|
Assert.DoesNotContain(plate.ClampZones, z => z.Id == "clamp-3");
|
|
Assert.Equal(new[] { "clamp-3" }, plate.RemovedClamps);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemovedClampStaysOffAfterResizeAndReappearsIfTheClampComesBackOntoTheSheet()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine(), new[] { "clamp-8" }); // centered at 111"
|
|
|
|
plate.Size = new Size(48, 60); // clamp-8 is beyond this sheet regardless
|
|
Assert.Equal(new[] { "clamp-1", "clamp-2", "clamp-3", "clamp-4" },
|
|
plate.ClampZones.Select(z => z.Id));
|
|
Assert.Equal(new[] { "clamp-8" }, plate.RemovedClamps);
|
|
|
|
plate.Size = new Size(48, 120); // clamp-8 is back on the sheet, but still removed
|
|
Assert.Equal(7, plate.ClampZones.Count);
|
|
Assert.DoesNotContain(plate.ClampZones, z => z.Id == "clamp-8");
|
|
}
|
|
|
|
[Fact]
|
|
public void ClampRemovalMinutesSumsTheRailsPerClampTime()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
Assert.Equal(0, plate.ClampRemovalMinutes); // no rail yet
|
|
|
|
plate.SetClampRail(new ClampRail(1.5, 0.625, MachineCenters, removalMinutes: 7),
|
|
new[] { "clamp-3", "clamp-4" });
|
|
Assert.Equal(14, plate.ClampRemovalMinutes);
|
|
|
|
plate.SetClampRail(null!);
|
|
Assert.Equal(0, plate.ClampRemovalMinutes); // detaching forgets the removed set too
|
|
Assert.Empty(plate.RemovedClamps);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetClampRailWithoutRemovedClampsBehavesAsBefore()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine());
|
|
|
|
Assert.Empty(plate.RemovedClamps);
|
|
Assert.Equal(8, plate.ClampZones.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void MatchesWithRemovedClampsComparesAgainstTheReducedResolution()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine(), new[] { "clamp-3" });
|
|
|
|
Assert.True(Machine().Matches(plate, new[] { "clamp-3" }));
|
|
Assert.False(Machine().Matches(plate)); // full resolution: zones don't match
|
|
Assert.False(Machine().Matches(plate, Array.Empty<string>()));
|
|
Assert.False(Machine().Matches(plate, new[] { "clamp-4" }));
|
|
}
|
|
// PM 6ded8ac4 (a): only removed clamps that are actually on this sheet cost time.
|
|
[Fact]
|
|
public void RemovalTimeCountsOnlyRemovedClampsOnTheSheet()
|
|
{
|
|
var plate = Sheet(48, 120);
|
|
plate.SetClampRail(Machine(), new[] { "clamp-3", "clamp-8", "clamp-99" });
|
|
Assert.Equal(2 * ClampRail.DefaultRemovalMinutes, plate.ClampRemovalMinutes); // 3 and 8
|
|
|
|
plate.Size = new Size(48, 60); // clamp-8 is now beyond the sheet
|
|
Assert.Equal(ClampRail.DefaultRemovalMinutes, plate.ClampRemovalMinutes);
|
|
Assert.Contains("clamp-8", plate.RemovedClamps); // still listed (C13)
|
|
}
|
|
|
|
// PM 6ded8ac4 (e): the removed set is read-only, even through a downcast, and an
|
|
// untouched plate does not share a mutable empty set with every other plate.
|
|
[Fact]
|
|
public void RemovedClampsCannotBeMutatedThroughTheCollection()
|
|
{
|
|
var removed = Sheet(48, 120);
|
|
removed.SetClampRail(Machine(), new[] { "clamp-3" });
|
|
var untouched = Sheet(48, 120);
|
|
|
|
Assert.True(((ICollection<string>)removed.RemovedClamps).IsReadOnly);
|
|
Assert.True(((ICollection<string>)untouched.RemovedClamps).IsReadOnly);
|
|
}
|
|
}
|