feat(engine): introduce whole-job nesting contracts
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
public class NestJobRunnerTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyJobCompletesWithoutPlatesOrPlacementWork()
|
||||
{
|
||||
var fake = new FakePlateNester();
|
||||
var factoryCalls = 0;
|
||||
var runner = new NestJobRunner(_ => { factoryCalls++; return fake; });
|
||||
var job = new NestJob(Array.Empty<NestJobPart>(), new[]
|
||||
{
|
||||
new NestPlateStock("finite", new Size(100, 200), 2),
|
||||
new NestPlateStock("unlimited", new Size(100, 200))
|
||||
});
|
||||
|
||||
var result = runner.Solve(job);
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(NestJobStopReason.Completed, result.StopReason);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Empty(result.Fulfillment);
|
||||
Assert.Collection(result.StockUsage,
|
||||
usage => { Assert.Equal(0, usage.Used); Assert.Equal(2, usage.Remaining); },
|
||||
usage => { Assert.Equal(0, usage.Used); Assert.Null(usage.Remaining); });
|
||||
Assert.Equal(0, factoryCalls);
|
||||
Assert.Equal(0, fake.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreCancelledEmptyJobThrows()
|
||||
{
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
var runner = new NestJobRunner(_ => new FakePlateNester());
|
||||
var job = new NestJob(Array.Empty<NestJobPart>(), Array.Empty<NestPlateStock>());
|
||||
Assert.Throws<OperationCanceledException>(() => runner.Solve(job, token: cancellation.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonemptyJobIsExplicitlyUnsupportedInContractSlice()
|
||||
{
|
||||
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 1);
|
||||
var job = new NestJob(new[] { part }, Array.Empty<NestPlateStock>());
|
||||
var runner = new NestJobRunner(_ => new FakePlateNester());
|
||||
Assert.Throws<NotSupportedException>(() => runner.Solve(job));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JobOwnsCollectionsSettingsAndExactGeometryIncludingHoleArc()
|
||||
{
|
||||
var program = TestDrawingFactory.Rectangle();
|
||||
program.MoveTo(3.123456789, 4);
|
||||
program.ArcTo(3.123456789, 4, 4, 4, RotationType.CW);
|
||||
var geometry = PartGeometrySnapshot.FromProgram(program);
|
||||
var parts = new List<NestJobPart> { new("p", geometry, 3) };
|
||||
var size = new Size(100, 200);
|
||||
var edges = new Spacing(1, 2, 3, 4);
|
||||
var stocks = new List<NestPlateStock> { new("s", size, 0, 2, edges, 3) };
|
||||
var job = new NestJob(parts, stocks);
|
||||
parts.Clear(); stocks.Clear(); program.Codes.Clear(); size.Width = 0; edges.Left = 999;
|
||||
|
||||
Assert.Single(job.Parts);
|
||||
Assert.Equal(3, job.Parts[0].Quantity);
|
||||
Assert.Equal(7, geometry.Motions.Count);
|
||||
Assert.Equal(CodeType.RapidMove, geometry.Motions[5].Type);
|
||||
Assert.Equal(CodeType.ArcMove, geometry.Motions[6].Type);
|
||||
Assert.Equal(3.123456789, geometry.Motions[6].X);
|
||||
Assert.Equal(4, geometry.Motions[6].CenterX);
|
||||
Assert.Equal(RotationType.CW, geometry.Motions[6].Rotation);
|
||||
Assert.Equal(100, job.Plates[0].Size.Width);
|
||||
Assert.Equal(1, job.Plates[0].EdgeSpacing.Left);
|
||||
Assert.Equal(0, job.Plates[0].Quantity);
|
||||
Assert.Equal("Default", job.Options.PlacementStrategy);
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<NestJobPart>)job.Parts).Clear());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyZeroStepMeansAutomaticNotFixed()
|
||||
{
|
||||
Assert.Equal(RotationPolicyKind.Automatic, RotationPolicy.FromLegacy(0, 1, 2).Kind);
|
||||
Assert.Equal(RotationPolicyKind.Fixed, RotationPolicy.Fixed(1).Kind);
|
||||
Assert.Equal(RotationPolicyKind.BoundedSweep, RotationPolicy.BoundedSweep(0, 1, 0.5).Kind);
|
||||
}
|
||||
|
||||
private sealed class FakePlateNester : IPlateNester
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
Calls++;
|
||||
return new PlateCandidate(Array.Empty<NestJobPlacement>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
internal static class TestDrawingFactory
|
||||
{
|
||||
public static Program Rectangle(double width = 10, double length = 20)
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(width, 0);
|
||||
program.LineTo(width, length);
|
||||
program.LineTo(0, length);
|
||||
program.LineTo(0, 0);
|
||||
return program;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<ProjectReference Include="../OpenNest.Engine/OpenNest.Engine.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Synchronous whole-job solver. Cancellation throws, rather than returning partial success.</summary>
|
||||
public interface INestingEngine
|
||||
{
|
||||
NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Places on one sheet only. Must not change stock, demand, or caller-owned domain objects.</summary>
|
||||
public interface IPlateNester
|
||||
{
|
||||
PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>One material/unit system's requirements. Collections are copied; all nested values are immutable.</summary>
|
||||
public sealed class NestJob
|
||||
{
|
||||
public NestJob(IEnumerable<NestJobPart> parts, IEnumerable<NestPlateStock> plates, NestJobOptions options = null)
|
||||
{
|
||||
Parts = Own(parts);
|
||||
Plates = Own(plates);
|
||||
Options = options ?? new NestJobOptions();
|
||||
if (Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count ||
|
||||
Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count)
|
||||
throw new ArgumentException("Part and stock IDs must each be unique.");
|
||||
}
|
||||
|
||||
public IReadOnlyList<NestJobPart> Parts { get; }
|
||||
public IReadOnlyList<NestPlateStock> Plates { get; }
|
||||
public NestJobOptions Options { get; }
|
||||
|
||||
internal static IReadOnlyList<T> Own<T>(IEnumerable<T> source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
var values = source.ToArray();
|
||||
if (values.Any(value => value is null))
|
||||
throw new ArgumentException("Null entries are not allowed.", nameof(source));
|
||||
return Array.AsReadOnly(values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Immutable per-job options; selection never changes the legacy global registry.</summary>
|
||||
public sealed class NestJobOptions
|
||||
{
|
||||
public NestJobOptions(string placementStrategy = "Default", int? maxPlates = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy);
|
||||
if (maxPlates <= 0) throw new ArgumentOutOfRangeException(nameof(maxPlates));
|
||||
PlacementStrategy = placementStrategy;
|
||||
MaxPlates = maxPlates;
|
||||
}
|
||||
|
||||
public string PlacementStrategy { get; }
|
||||
/// <summary>Maximum physical sheets to commit, or null for no explicit cap.</summary>
|
||||
public int? MaxPlates { get; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>An immutable requirement, independent of drawing names, UI state, and drawing quantity counters.</summary>
|
||||
public sealed class NestJobPart
|
||||
{
|
||||
public NestJobPart(string id, PartGeometrySnapshot geometry, int quantity, int priority = 0,
|
||||
RotationPolicy rotation = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentNullException.ThrowIfNull(geometry);
|
||||
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Geometry = geometry;
|
||||
Quantity = quantity;
|
||||
Priority = priority;
|
||||
Rotation = rotation ?? RotationPolicy.Automatic;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public PartGeometrySnapshot Geometry { get; }
|
||||
/// <summary>Positive number requested; never decremented by placement code.</summary>
|
||||
public int Quantity { get; }
|
||||
public int Priority { get; }
|
||||
public RotationPolicy Rotation { get; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum NestJobStatus { Complete, Incomplete }
|
||||
public enum NestJobStopReason { Completed, StockExhausted, NoPlacementFound, PlateLimitReached }
|
||||
public enum NestJobStage { EvaluatingCandidate, PlateCommitted }
|
||||
|
||||
/// <summary>Committed counts only; candidate evaluation does not imply committed production.</summary>
|
||||
public sealed record NestJobProgress(NestJobStage Stage, string StockId, int PlateIndex,
|
||||
int CommittedPlates, int CommittedParts);
|
||||
|
||||
/// <summary>
|
||||
/// Rotate about the snapshot origin, then translate by X/Y into the selected plate quadrant frame.
|
||||
/// Rotation is in radians. InstanceIndex is zero-based and unique within a part requirement across the job.
|
||||
/// The runner assigns final instance indices when committing a candidate.
|
||||
/// </summary>
|
||||
public sealed record NestJobPlacement(string PartId, int InstanceIndex, double X, double Y, double Rotation);
|
||||
|
||||
/// <summary>Requested = Placed + Unplaced for a requirement ID.</summary>
|
||||
public sealed record PartFulfillment(string PartId, int Requested, int Placed, int Unplaced);
|
||||
|
||||
/// <summary>Used counts physical sheets; Remaining is null only for unlimited stock.</summary>
|
||||
public sealed record StockUsage(string StockId, int Used, int? Remaining);
|
||||
|
||||
/// <summary>One physical sheet, with owned ordered placements and immutable stock/settings snapshot.</summary>
|
||||
public sealed class NestJobPlateResult
|
||||
{
|
||||
public NestJobPlateResult(int plateIndex, NestPlateStock stock, IEnumerable<NestJobPlacement> placements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stock);
|
||||
PlateIndex = plateIndex;
|
||||
Stock = stock;
|
||||
Placements = NestJob.Own(placements);
|
||||
}
|
||||
|
||||
public int PlateIndex { get; }
|
||||
public string StockId => Stock.Id;
|
||||
public NestPlateStock Stock { get; }
|
||||
public IReadOnlyList<NestJobPlacement> Placements { get; }
|
||||
}
|
||||
|
||||
/// <summary>Detached result values in commit/input order; no mutable Drawing, Plate, or NestItem escapes.</summary>
|
||||
public sealed class NestJobResult
|
||||
{
|
||||
public NestJobResult(NestJobStatus status, NestJobStopReason stopReason,
|
||||
IEnumerable<NestJobPlateResult> plates, IEnumerable<PartFulfillment> fulfillment,
|
||||
IEnumerable<StockUsage> stockUsage)
|
||||
{
|
||||
Status = status;
|
||||
StopReason = stopReason;
|
||||
Plates = NestJob.Own(plates);
|
||||
Fulfillment = NestJob.Own(fulfillment);
|
||||
StockUsage = NestJob.Own(stockUsage);
|
||||
}
|
||||
|
||||
public NestJobStatus Status { get; }
|
||||
public NestJobStopReason StopReason { get; }
|
||||
public IReadOnlyList<NestJobPlateResult> Plates { get; }
|
||||
public IReadOnlyList<PartFulfillment> Fulfillment { get; }
|
||||
public IReadOnlyList<StockUsage> StockUsage { get; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Contract-stage runner: empty jobs only. Nonempty allocation is not implemented yet.</summary>
|
||||
public sealed class NestJobRunner : INestingEngine
|
||||
{
|
||||
private readonly Func<string, IPlateNester> plateNesterFactory;
|
||||
|
||||
/// <summary>Stores a runner-local strategy factory; never consults the global engine registry.</summary>
|
||||
public NestJobRunner(Func<string, IPlateNester> plateNesterFactory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plateNesterFactory);
|
||||
this.plateNesterFactory = plateNesterFactory;
|
||||
}
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (job.Parts.Count != 0)
|
||||
throw new NotSupportedException("Whole-job allocation is not implemented yet; only empty jobs are supported.");
|
||||
return new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||
Array.Empty<NestJobPlateResult>(), Array.Empty<PartFulfillment>(),
|
||||
job.Plates.Select(stock => new StockUsage(stock.Id, 0, stock.Quantity)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Immutable stock settings. Size and spacing are copied value types, not caller-owned settings.</summary>
|
||||
public sealed class NestPlateStock
|
||||
{
|
||||
public NestPlateStock(string id, Size size, int? quantity = null, double partSpacing = 0,
|
||||
Spacing edgeSpacing = default, int quadrant = 1)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Size = size;
|
||||
Quantity = quantity;
|
||||
PartSpacing = partSpacing;
|
||||
EdgeSpacing = edgeSpacing;
|
||||
Quadrant = quadrant;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public Size Size { get; }
|
||||
/// <summary>Available physical sheets: null is unlimited, zero is legal but unavailable.</summary>
|
||||
public int? Quantity { get; }
|
||||
public double PartSpacing { get; }
|
||||
public Spacing EdgeSpacing { get; }
|
||||
public int Quadrant { get; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Exact immutable CNC motion values. Rapid moves retain contour/hole boundaries; arcs are not tessellated.</summary>
|
||||
public sealed record PartGeometryMotion(CodeType Type, double X, double Y, double CenterX,
|
||||
double CenterY, RotationType Rotation, LayerType Layer, bool Suppressed);
|
||||
|
||||
/// <summary>
|
||||
/// Owned geometry only: no Drawing, quantity, events, or mutable CNC references are retained.
|
||||
/// This initial boundary supports flat rapid/linear/arc programs and rejects other instructions explicitly.
|
||||
/// Coordinates and mode are preserved without normalization, rounding, or polygon approximation.
|
||||
/// </summary>
|
||||
public sealed class PartGeometrySnapshot
|
||||
{
|
||||
private PartGeometrySnapshot(Mode mode, IEnumerable<PartGeometryMotion> motions)
|
||||
{
|
||||
Mode = mode;
|
||||
Motions = NestJob.Own(motions);
|
||||
}
|
||||
|
||||
public Mode Mode { get; }
|
||||
public IReadOnlyList<PartGeometryMotion> Motions { get; }
|
||||
|
||||
/// <summary>Copies supported motion geometry immediately; later program edits cannot affect this snapshot.</summary>
|
||||
public static PartGeometrySnapshot FromProgram(Program program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
var motions = program.Codes.Select(code => code switch
|
||||
{
|
||||
ArcMove arc => new PartGeometryMotion(arc.Type, arc.EndPoint.X, arc.EndPoint.Y,
|
||||
arc.CenterPoint.X, arc.CenterPoint.Y, arc.Rotation, arc.Layer, arc.Suppressed),
|
||||
LinearMove line => new PartGeometryMotion(line.Type, line.EndPoint.X, line.EndPoint.Y,
|
||||
0, 0, default, line.Layer, line.Suppressed),
|
||||
RapidMove rapid => new PartGeometryMotion(rapid.Type, rapid.EndPoint.X, rapid.EndPoint.Y,
|
||||
0, 0, default, default, rapid.Suppressed),
|
||||
_ => throw new NotSupportedException("Geometry snapshots currently support only flat rapid/linear/arc programs.")
|
||||
});
|
||||
return new PartGeometrySnapshot(program.Mode, motions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Owned candidate poses only; not committed fulfillment or inventory accounting.</summary>
|
||||
public sealed class PlateCandidate
|
||||
{
|
||||
public PlateCandidate(IEnumerable<NestJobPlacement> placements) => Placements = NestJob.Own(placements);
|
||||
public IReadOnlyList<NestJobPlacement> Placements { get; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Read-only stock settings and remaining requirements for a single candidate trial.</summary>
|
||||
public sealed class PlatePlacementRequest
|
||||
{
|
||||
public PlatePlacementRequest(NestPlateStock stock, IEnumerable<NestJobPart> parts)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stock);
|
||||
Stock = stock;
|
||||
Parts = NestJob.Own(parts);
|
||||
}
|
||||
|
||||
public NestPlateStock Stock { get; }
|
||||
public IReadOnlyList<NestJobPart> Parts { get; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum RotationPolicyKind { Fixed, BoundedSweep, Automatic }
|
||||
|
||||
/// <summary>Immutable rotation constraints, in radians about the geometry origin.</summary>
|
||||
public sealed class RotationPolicy
|
||||
{
|
||||
private RotationPolicy(RotationPolicyKind kind, double start, double end, double step)
|
||||
{
|
||||
if (!double.IsFinite(start) || !double.IsFinite(end) || !double.IsFinite(step))
|
||||
throw new ArgumentException("Angles must be finite.");
|
||||
Kind = kind;
|
||||
Start = start;
|
||||
End = end;
|
||||
Step = step;
|
||||
}
|
||||
|
||||
public RotationPolicyKind Kind { get; }
|
||||
public double Start { get; }
|
||||
public double End { get; }
|
||||
public double Step { get; }
|
||||
public static RotationPolicy Automatic { get; } = new(RotationPolicyKind.Automatic, 0, 0, 0);
|
||||
public static RotationPolicy Fixed(double angle) => new(RotationPolicyKind.Fixed, angle, angle, 0);
|
||||
public static RotationPolicy BoundedSweep(double start, double end, double step)
|
||||
{
|
||||
if (step <= 0 || end < start) throw new ArgumentException("Sweep needs a positive step and ordered bounds.");
|
||||
return new RotationPolicy(RotationPolicyKind.BoundedSweep, start, end, step);
|
||||
}
|
||||
|
||||
/// <summary>Preserves the legacy zero-step automatic sentinel; zero never means locked rotation.</summary>
|
||||
public static RotationPolicy FromLegacy(double stepAngle, double rotationStart, double rotationEnd) =>
|
||||
stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle);
|
||||
}
|
||||
@@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.GravographIS
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Engine.Tests", "OpenNest.Engine.Tests\OpenNest.Engine.Tests.csproj", "{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -200,6 +202,18 @@ Global
|
||||
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F2EF39E0-1A95-4C32-B50B-3D71EC72F692}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -64,6 +64,18 @@ cd OpenNest
|
||||
dotnet build OpenNest.sln
|
||||
```
|
||||
|
||||
### Cross-platform engine contract tests
|
||||
|
||||
```bash
|
||||
dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj
|
||||
```
|
||||
|
||||
`OpenNest.Engine.Tests` targets `net8.0` and runs on Linux, macOS, and Windows without the desktop project or local DXF fixtures. The existing `OpenNest.Tests` suite still requires Windows.
|
||||
|
||||
The new whole-job contracts in `OpenNest.Engine/Jobs` (`namespace OpenNest`) use owned immutable geometry/settings, explicit part IDs and positive demand, finite or unlimited stock (`null` means unlimited; zero means unavailable), and result ID/pose values rather than mutable desktop models. Rotation is in radians about the geometry origin, followed by translation into the plate quadrant frame. Strategy factories belong to each runner, not the global registry.
|
||||
|
||||
**Current scope:** `NestJobRunner.Solve` completes empty jobs without consuming stock and honors initial cancellation by throwing. Nonempty jobs explicitly throw `NotSupportedException`; allocation, legacy adapters, placement validation, and production strategy resolution are not implemented yet. Geometry snapshots currently preserve flat CNC rapid/line/arc programs, including hole contours, without approximation; other instructions are explicitly rejected. Existing desktop, API, CLI, and MCP nesting paths are unchanged.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
@@ -142,7 +154,8 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- project.zip ext
|
||||
OpenNest.sln
|
||||
├── OpenNest/ # WinForms desktop application (UI)
|
||||
├── OpenNest.Core/ # Domain model, geometry, and CNC primitives
|
||||
├── OpenNest.Engine/ # Nesting algorithms (fill, pack, compact, best-fit)
|
||||
├── OpenNest.Engine/ # Nesting algorithms and whole-job contracts
|
||||
├── OpenNest.Engine.Tests/ # Cross-platform whole-job contract tests (net8.0)
|
||||
├── OpenNest.IO/ # File I/O — DXF import/export, nest file format
|
||||
├── OpenNest.Console/ # Command-line interface for batch nesting
|
||||
├── OpenNest.Api/ # Programmatic nesting API (NestRunner pipeline)
|
||||
|
||||
Reference in New Issue
Block a user