Files
OpenNest/OpenNest.Engine/Jobs/NestJobValidator.cs
T
aj bafb311735 feat(clamps): keep the whole footprint of a partly used clamp
A clamp is an avoidance area, so a clamp that only partly reaches a
sheet (for example centered at 60.5" on a 60" sheet, or near the
origin) now keeps its whole 1.5" x 0.625" footprint instead of being
clipped to the sheet; clamps wholly beyond the sheet are still left
off. Job validation accepts a zone that hangs past the sheet edges and
still rejects one that does not reach the sheet at all. Both packing
lanes already block keep-outs extending past the bin; a hanging-clamp
solve through Rectangles, Irregular and Default keeps every part clear
of the whole inflated footprint.

User decision 2026-10-12, settling the plan's pending D2 question.
2026-10-13 18:15:49 -04:00

127 lines
4.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.Jobs;
/// <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);
ValidateStockCosts(job.Plates);
foreach (var stock in job.Plates)
{
var edges = stock.EdgeSpacing;
if (
!Positive(stock.Area)
|| !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)
);
ValidateClampZones(stock, 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
{
_ = JobPartGeometry.Read(part.Geometry);
}
catch (ArgumentException exception)
{
throw new ArgumentException(
$"Geometry must contain usable closed edges: {part.Id}. {exception.Message}",
nameof(job),
exception
);
}
}
}
public static void ValidateStockCosts(IEnumerable<NestPlateStock> stocks)
{
var available = stocks.Where(s => s.Quantity != 0).ToArray();
if (available.Any(s => s.Cost.HasValue) && available.Any(s => !s.Cost.HasValue))
throw new ArgumentException("Specify a positive cost for every available stock row or omit all costs. Missing: "
+ string.Join(", ", available.Where(s => !s.Cost.HasValue).Select(s => s.Id)));
}
/// <summary>
/// Every stock zone must be uniquely identified and reach onto its own sheet (a positive
/// overlap). A clamp is an avoidance area, so its whole footprint may hang past the sheet
/// edges; a zone wholly off the sheet is malformed. Geometry and finiteness are already
/// enforced by <see cref="ClampZone"/>. Zones are compared in the placement frame, where
/// the stock carries them.
/// </summary>
private static void ValidateClampZones(NestPlateStock stock, NestJob job)
{
if (stock.ClampZones.Count == 0)
return;
var ids = new HashSet<string>(StringComparer.Ordinal);
var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
foreach (var zone in stock.ClampZones)
{
if (!ids.Add(zone.Id))
throw new ArgumentException(
$"Clamp zone id '{zone.Id}' is defined twice on stock {stock.Id}.",
nameof(job)
);
var b = zone.Bounds;
if (
!(b.Right > left)
|| !(b.Left < left + stock.Size.Length)
|| !(b.Top > bottom)
|| !(b.Bottom < bottom + stock.Size.Width)
)
throw new ArgumentException(
$"Clamp zone '{zone.Id}' on stock {stock.Id} lies outside the sheet.",
nameof(job)
);
}
}
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;
}