using System;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.Diagnostics;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Geometry;
using Xunit;
namespace OpenNest.Engine.Tests.Jobs;
///
/// Every stores clamp zones in the sheet's lower-left frame, including
/// plates the engine builds from stock; consumers map them into the signed placement frame
/// through . Fixtures use a real rail in all quadrants, with
/// expected footprints from the physical rule (center +/- width/2 from the origin along the
/// edge, depth into the sheet).
///
public class ClampZoneSheetFrameTests
{
private static (string, long, long, long, long) Bits(ClampZone zone) =>
(zone.Id, BitConverter.DoubleToInt64Bits(zone.Bounds.X), BitConverter.DoubleToInt64Bits(zone.Bounds.Y),
BitConverter.DoubleToInt64Bits(zone.Bounds.Length), BitConverter.DoubleToInt64Bits(zone.Bounds.Width));
private static Box World(int quadrant, double center, double width = 1.5, double depth = 0.625)
{
var x = quadrant is 2 or 3 ? -(center + width / 2) : center - width / 2;
var y = quadrant is 3 or 4 ? -depth : 0;
return new Box(x, y, width, depth);
}
private static Program Rectangle(double length, double width)
{
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(length, 0);
program.LineTo(length, width);
program.LineTo(0, width);
program.LineTo(0, 0);
return program;
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void MaterializedPlatesStoreSheetFrameZonesAndWarnForCutsThroughThem(int quadrant)
{
var live = new Plate(81, 120) { Quadrant = quadrant };
live.SetClampRail(new ClampRail(1.5, 0.625, new[] { 35.0, 120.5 }));
var stock = DrawingJobMapper.FromPlate("s", live, 1);
var job = new NestJob(Array.Empty(), new[] { stock });
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, stock, Array.Empty()) },
Array.Empty(), Array.Empty());
var nest = NestResultMaterializer.Materialize(job, result).Nest;
var plate = Assert.Single(nest.Plates);
Assert.Equal(live.ClampZones.Select(Bits), plate.ClampZones.Select(Bits));
Assert.Equal(stock.ClampZones.Select(Bits), ClampZoneFrame.ToPlacement(plate).Select(Bits));
var footprint = World(quadrant, 35);
Assert.Equal(footprint.Left, stock.ClampZones[0].Bounds.Left, 9);
Assert.Equal(footprint.Bottom, stock.ClampZones[0].Bounds.Bottom, 9);
// A short cut-off through the physical clamp must be found and require consent.
plate.CutOffs.Add(new CutOff(new Vector(quadrant is 2 or 3 ? -35 : 35, 0), CutOffAxis.Vertical)
{
StartLimit = quadrant is 3 or 4 ? -2 : 0,
EndLimit = quadrant is 3 or 4 ? 0 : 2,
});
Assert.Equal("clamp-1", Assert.Single(ClampCutDiagnostics.Find(plate)).ClampId);
plate.RegenerateCutOffs(new CutOffSettings());
var report = PostVerificationAnalyzer.Analyze(nest);
Assert.Contains(report.Findings, f => f.ClampId == "clamp-1");
Assert.False(report.CanPost(false));
}
[Fact]
public void StockClampZonesCannotBeEditedThroughTheReturnedZones()
{
// Stock is immutable input: editing a returned zone must not move the stock's clamp
// away from the zones its materialized plates (validation, fill, posting) use.
var live = new Plate(24, 48);
live.ClampZones.Add(new ClampZone("z", new Box(10, 0, 1.5, 0.625)));
var stock = DrawingJobMapper.FromPlate("s", live, 1);
stock.ClampZones[0].Bounds.X += 10;
foreach (var zone in stock.ClampZones)
zone.Bounds.Y = 5;
var current = Assert.Single(stock.ClampZones);
Assert.Equal(10, current.Bounds.X);
Assert.Equal(0, current.Bounds.Y);
var plate = DrawingJobMapper.CreatePlate(stock);
Assert.Equal(stock.ClampZones.Select(Bits), ClampZoneFrame.ToPlacement(plate).Select(Bits));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void InteractiveFillOnLiveRailPlateAvoidsClampsInEveryQuadrant(int quadrant)
{
// Five rows of 4.29 squares at 0.5 spacing span 23.45 of the 23.5 work-area height, so an
// unobstructed fill reaches the front edge in every quadrant.
var plate = new Plate(new Size(24, 48))
{
Quadrant = quadrant,
EdgeSpacing = new Spacing(0.25, 0.25),
PartSpacing = 0.5,
};
var centers = new[] { 10.75, 20.75, 30.75, 40.75 };
plate.SetClampRail(new ClampRail(1.5, 0.625, centers));
var keepOuts = centers.Select(c => World(quadrant, c).Offset(0.25)).ToArray();
var item = new NestItem { Drawing = new Drawing("square", Rectangle(4.29, 4.29)), Quantity = 500 };
var parts = PlateFillService.FillItem("Fill", plate, item, plate.WorkArea(), null, CancellationToken.None);
Assert.True(parts.Count >= 20, $"only {parts.Count} parts placed");
foreach (var part in parts)
{
var box = part.BoundingBox;
Assert.DoesNotContain(keepOuts, k => box.Left < k.Right - 1e-6 && k.Left < box.Right - 1e-6
&& box.Bottom < k.Top - 1e-6 && k.Bottom < box.Top - 1e-6);
}
}
}