using OpenNest.Engine.Testing;
using static OpenNest.Engine.Testing.JobBuilder;
using static OpenNest.Engine.Testing.Shapes;
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
namespace OpenNest.Engine.Qwen38FlashNext.Tests;
///
/// Starter acceptance tests. Every layout is checked by the same NestValidator the benchmark
/// scores with, so a passing test means the benchmark will accept the layout. They fail until
/// Solve() is implemented; add engine-specific tests alongside them.
///
public class Qwen38FlashNextNestingEngineTests
{
[Fact]
public void HasPublicParameterlessConstructorForPluginDiscovery()
{
var engine = Activator.CreateInstance(typeof(Qwen38FlashNextNestingEngine));
Assert.IsAssignableFrom(engine);
}
[Fact]
public void RectanglesFitOnOneSheetWithSpacing()
{
var job = Job(new[] { Part("rect", Rectangle(10, 5), 12) }, new[] { Stock("sheet", 48, 96, spacing: 0.25) });
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Single(result.Plates);
Assert.Equal(12, result.Plates[0].Placements.Count);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void MixedArcAndConcavePartsAreValidInEveryQuadrant(int quadrant)
{
var job = Job(
new[]
{
Part("disc", Disc(3), 10),
Part("ell", LShape(12, 8, 4), 10),
Part("tri", Triangle(9, 6), 10),
},
new[] { Stock("sheet", 40, 60, spacing: 0.5, edge: new Spacing(0.5, 0.5, 0.5, 0.5), quadrant: quadrant) }
);
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
}
[Fact]
public void RotatedConcavePartsKeepSpacingAtFixedAngles()
{
// Regression: the per-orientation spacing inflation must live in the rotated
// frame. L-shapes pinned to 90/270 degrees exercise exactly the orientations
// where an unrotated inflation misrepresents the material and lets parts
// rest closer than the spacing.
var l = Part(
"l90",
LShape(12, 8, 4),
8,
RotationPolicy.Fixed(System.Math.PI / 2, allow180Equivalent: true)
);
var job = Job(new[] { l }, new[] { Stock("sheet", 40, 60, spacing: 0.5) });
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
}
[Fact]
public void OverflowSpillsOntoAdditionalSheets()
{
var job = Job(new[] { Part("square", Rectangle(10, 10), 30) }, new[] { Stock("sheet", 25, 45, spacing: 0.25) });
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.True(result.Plates.Count > 1);
}
[Fact]
public void PartTooBigForAnySheetIsReportedUnplaced()
{
var job = Job(
new[] { Part("huge", Rectangle(50, 50), 1), Part("small", Rectangle(5, 5), 4) },
new[] { Stock("sheet", 20, 20, spacing: 0.25) }
);
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
var huge = Assert.Single(result.Fulfillment, f => f.PartId == "huge");
Assert.Equal(1, huge.Unplaced);
}
[Fact]
public void EtchMarksAreLeftOutOfNestingGeometry()
{
// A bend tick starts on material and ends 1.0 into a side notch, outside the part but
// inside its bounding box (the PEP case that crashed nesting before 1b5e1b1). As
// material it is open geometry leaving the part; as a mark it must be ignored.
var etched = Polyline((0, 0), (10, 0), (10, 4), (8, 4), (8, 6), (10, 6), (10, 10), (0, 10));
etched.Codes.Add(new RapidMove(7.5, 5));
etched.Codes.Add(new LinearMove(9, 5) { Layer = LayerType.Scribe });
var job = Job(new[] { Part("part", etched, 2, RotationPolicy.Fixed(0)) }, new[] { Stock("sheet", 10.4, 20.6, spacing: 0.2) });
var result = new Qwen38FlashNextNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
}
[Fact]
public void PlateIndicesRunInCommitOrder()
{
// Every sheet comes from the same stock (index 0), so the stock index must not leak
// into PlateIndex: the host and OpenNest.Api treat it as the sheet's position.
var job = Job(new[] { Part("square", Rectangle(10, 10), 30) }, new[] { Stock("sheet", 25, 45, spacing: 0.25) });
var result = new Qwen38FlashNextNestingEngine().Solve(job);
Assert.True(result.Plates.Count > 1);
Assert.Equal(Enumerable.Range(0, result.Plates.Count), result.Plates.Select(p => p.PlateIndex));
}
}
public sealed class Qwen38FlashNextContractTests : EngineContractTests { }