Files
OpenNest/OpenNest.Engine.Tests/Jobs/ClampClearanceTests.cs
T
ajandClaude Opus 5.5 8a5f16069d fix(engine): check materialized plates against the stock's clamp rail
CreatePlate copied a rail-backed stock's zones but not its rail, so the
materialized plate fell back to the bottom edge spacing as its clamp
clearance while every engine packed to the rail's own. With a rail
clearance below the edge spacing, valid layouts from all engines were
rejected as clamp violations. Carry the rail and any removed clamps
across, sharing the rule the commit path already used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:20:34 -05:00

129 lines
4.5 KiB
C#

using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
using Xunit;
namespace OpenNest.Engine.Tests.Jobs;
/// <summary>
/// A clamp rail's own clearance replaces the bottom edge spacing as the gap parts keep from
/// clamps, in the stock engines read, the layout check and the interactive push; part spacing
/// never applies to clamps.
/// </summary>
public class ClampClearanceTests
{
// 24 x 24 sheet, 0.5 edge spacing, 1.0 part spacing; one clamp at X 6 on the front edge.
private const double Edge = 0.5;
private const double ClampTop = 0.625;
private static Plate RailPlate(double? clearance)
{
var plate = new Plate(new Size(24, 24))
{
EdgeSpacing = new Spacing(Edge, Edge),
PartSpacing = 1.0,
};
plate.SetClampRail(new ClampRail(1.5, ClampTop, new[] { 6.0 }, clearance: clearance));
return plate;
}
private static Drawing Square(double size)
{
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(size, 0);
program.LineTo(size, size);
program.LineTo(0, size);
program.LineTo(0, 0);
return new Drawing("square", program);
}
private static List<string> Violations(Plate plate, Part part) =>
NestLayoutCheck.Validate(new() { (plate, new List<Part> { part }) },
new Dictionary<Drawing, (string, int)> { [part.BaseDrawing] = ("square", 1) });
[Fact]
public void WithoutItsOwnClearanceTheRailKeepsTheBottomEdgeSpacing()
{
var plate = RailPlate(clearance: null);
Assert.Equal(Edge, plate.ClampClearance);
Assert.Equal(Edge, DrawingJobMapper.FromPlate("s", plate, 1).ClampClearance);
}
[Fact]
public void StockCarriesTheRailsOwnClearance()
{
var plate = RailPlate(clearance: 0.05);
Assert.Equal(0.05, plate.ClampClearance);
Assert.Equal(0.05, DrawingJobMapper.FromPlate("s", plate, 1).ClampClearance);
}
[Fact]
public void AMaterializedPlateKeepsTheStocksRailAndItsRemovedClamps()
{
var source = RailPlate(clearance: 0.05);
source.SetClampRail(new ClampRail(1.5, ClampTop, new[] { 6.0, 12.0 }, clearance: 0.05), new[] { "clamp-1" });
var stock = DrawingJobMapper.FromPlate("s", source, 1);
var plate = DrawingJobMapper.CreatePlate(stock);
Assert.Equal(stock.ClampClearance, plate.ClampClearance);
Assert.Equal(new[] { "clamp-1" }, plate.RemovedClamps);
Assert.Equal("clamp-2", Assert.Single(plate.ClampZones).Id);
}
[Fact]
public void AMaterializedPlateKeepsZonesTheRailDoesNotRecreate()
{
var rail = new ClampRail(1.5, ClampTop, new[] { 6.0 }, clearance: 0.05);
var stock = new NestPlateStock("s", new Size(24, 24), 1, 1.0, new Spacing(Edge, Edge), 1, null,
new[] { new ClampZone("manual", new Box(9, 0, 2, 1)) })
{ ClampRail = rail };
var plate = DrawingJobMapper.CreatePlate(stock);
Assert.Equal("manual", Assert.Single(plate.ClampZones).Id);
}
[Fact]
public void LayoutCheckEnforcesTheClampClearanceNotTheEdgeSpacing()
{
// A part 0.1 above the clamp, straddling it in X.
var part = new Part(Square(3), new Vector(5, ClampTop + 0.1));
Assert.Contains(Violations(RailPlate(clearance: null), part), v => v.Contains("clamp zone"));
Assert.DoesNotContain(Violations(RailPlate(clearance: 0.05), part), v => v.Contains("clamp zone"));
Assert.Contains(Violations(RailPlate(clearance: 0.2), part), v => v.Contains("clamp zone"));
}
[Fact]
public void PushStopsAtTheClampClearanceRatherThanThePartSpacing()
{
var plate = RailPlate(clearance: 0);
var mover = new Part(Square(3), new Vector(5, 8));
plate.Parts.Add(mover);
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
// Part spacing (1.0) and edge spacing (0.5) would both stop it higher.
Assert.InRange(mover.BoundingBox.Bottom, ClampTop - 1e-6, ClampTop + 0.01);
}
[Fact]
public void PushStillKeepsTheClampClearanceWhenItExceedsThePartSpacing()
{
var plate = RailPlate(clearance: 2);
var mover = new Part(Square(3), new Vector(5, 8));
plate.Parts.Add(mover);
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
Assert.InRange(mover.BoundingBox.Bottom, ClampTop + 2 - 0.01, ClampTop + 2 + 0.01);
}
}