Files
OpenNest/OpenNest.Engine/Jobs/NestJobValidator.cs
T
aj 2b0b962c8f fix(engine): enforce whole-job safety invariants
Task 5 of the whole-job engine API: add a geometry safety gate that
validates every candidate trial before the runner commits accounting.

- NestJobPlacementValidator: closed-contour validity, rotation-policy
  compliance, work-area containment per quadrant, hole-aware material
  overlap, and required part spacing. Overlap is interior-only, so
  zero-clearance edge/corner contact remains a valid placement.
- NestJobValidator: route candidate validation through the geometry
  gate; reject unusable/unclosed/degenerate contours up front.
- NestJobRunner: wrap candidate evaluation in a progress bridge that
  tags legacy engine detail with the current candidate context.
- LegacyPlateNesterAdapter: forward IProgress to the legacy engine so
  its progress surfaces under the active candidate.
- Tests: geometry (quadrants, rotations, touching, containment, holes,
  empty stock, real Default/Strip smoke), validation, and cancellation
  suites; repaired test fakes that emitted out-of-bounds or overlapping
  placements the gate now correctly rejects.

Engine.Tests: 70 passed, 0 failed, 0 skipped in Debug and Release.
Windows-only OpenNest.Tests not run on Linux.
2026-09-18 05:56:37 -04:00

48 lines
2.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest;
/// <summary>Basic input and candidate accounting checks, NOT a geometry/clearance safety gate.</summary>
public static class NestJobValidator
{
public static void Validate(NestJob job)
{
ArgumentNullException.ThrowIfNull(job);
foreach (var stock in job.Plates)
{
var edges = stock.EdgeSpacing;
if (!Positive(stock.Size.Width) || !Positive(stock.Size.Length) ||
!Nonnegative(stock.PartSpacing) || !Nonnegative(edges.Left) || !Nonnegative(edges.Right) ||
!Nonnegative(edges.Top) || !Nonnegative(edges.Bottom) || stock.Quadrant < 1 || stock.Quadrant > 4 ||
edges.Left + edges.Right >= stock.Size.Length || edges.Top + edges.Bottom >= stock.Size.Width)
throw new ArgumentException($"Invalid stock dimensions/settings: {stock.Id}.", nameof(job));
}
foreach (var part in job.Parts)
{
if (part.Geometry.Motions.Count == 0 || part.Geometry.Motions.Any(m =>
!double.IsFinite(m.X) || !double.IsFinite(m.Y) ||
!double.IsFinite(m.CenterX) || !double.IsFinite(m.CenterY)))
throw new ArgumentException($"Geometry must contain finite motions: {part.Id}.", nameof(job));
try
{
NestJobPlacementValidator.ValidateGeometry(part.Geometry);
}
catch (ArgumentException exception)
{
throw new ArgumentException($"Geometry must contain usable closed edges: {part.Id}.", nameof(job), exception);
}
}
}
internal static void ValidateCandidate(PlateCandidate candidate, NestPlateStock stock,
IReadOnlyDictionary<string, int> remaining, IReadOnlyDictionary<string, NestJobPart> parts)
{
NestJobPlacementValidator.ValidateCandidate(candidate, stock, remaining, parts);
}
private static bool Positive(double value) => double.IsFinite(value) && value > 0;
private static bool Nonnegative(double value) => double.IsFinite(value) && value >= 0;
}