#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Math;
namespace OpenNest.Engine.Jobs.CommonLines;
///
/// The common-line trial runs beside the ordinary solve when the
/// job's is on: every part that allows common lines
/// is offered as kerf-gap pairs () plus its odd copy, so any engine
/// places them without knowing about common lines. Placed pairs expand into their two copies and
/// one explicit group each; the expanded layout is validated again.
///
internal sealed class CommonLinePipelinePrepass : IPipelinePrepass
{
/// Every pair proxy's part id starts with this; ordinary parts never do.
internal const string ProxyPrefix = "__common-line-pair-";
/// How many of a part's copies a trial offers as pairs.
internal enum Share
{
/// As many pairs as the quantity allows.
All,
/// About half the copies paired, the rest single: rows that fit an odd count.
Half,
}
private sealed record Proxy(string PartId, CommonLinePair Pair);
private readonly IReadOnlyDictionary proxies;
private readonly double kerf;
private CommonLinePipelinePrepass(NestJob engineJob, IReadOnlyDictionary proxies, double kerf)
{
EngineJob = engineJob;
this.proxies = proxies;
this.kerf = kerf;
}
public NestJob EngineJob { get; }
public string ReconciliationFailure => "Common-line engine output failed reconciliation";
/// True for a job holding pair proxies: a common-line trial's engine job.
internal static bool HasProxies(NestJob job) =>
job.Parts.Any(p => p.Id.StartsWith(ProxyPrefix, StringComparison.Ordinal));
///
/// The trial job, or null when the job does not use common lines or no part can pair
/// (none allows it, has two copies, may turn half a turn and has a long enough straight edge).
///
internal static CommonLinePipelinePrepass? Prepare(NestJob original, Share share, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(original);
token.ThrowIfCancellationRequested();
if (!original.Options.UsesCommonLines || original.Plates.Count == 0)
return null;
var kerf = original.Options.CommonLineKerf;
// A pair a kerf apart only saves anything where the part spacing is wider than the kerf.
if (original.Plates.All(stock => !(kerf < stock.PartSpacing)))
return null;
var ids = original.Parts.Select(p => p.Id).ToHashSet(StringComparer.Ordinal);
var proxies = new Dictionary(StringComparer.Ordinal);
var parts = new List();
foreach (var part in original.Parts)
{
token.ThrowIfCancellationRequested();
var pairs = Pairs(part, share);
var pair = pairs > 0 && CanTurnHalf(part.Rotation) ? CommonLinePair.Build(part.Geometry, kerf) : null;
if (pair == null)
{
parts.Add(part);
continue;
}
var proxyId = ProxyPrefix + part.Id;
if (ids.Contains(proxyId))
throw new InvalidOperationException("Common-line proxy ID collides with a requirement.");
proxies.Add(proxyId, new Proxy(part.Id, pair));
// The pair first: it is the larger piece, and engines keep input order among equals.
parts.Add(new NestJobPart(proxyId, pair.Geometry, pairs, part.Priority, part.Rotation));
var singles = part.Quantity - 2 * pairs;
if (singles > 0)
parts.Add(new NestJobPart(part.Id, part.Geometry, singles, part.Priority, part.Rotation)
{
AllowCommonLine = part.AllowCommonLine,
});
}
if (proxies.Count == 0)
return null;
var engineJob = new NestJob(parts, original.Plates, original.Options);
try
{
NestJobValidator.Validate(engineJob);
}
catch (ArgumentException)
{
return null; // A trial the engine could not be asked is no trial; the ordinary solve stands.
}
return new CommonLinePipelinePrepass(engineJob, proxies, kerf);
}
private static int Pairs(NestJobPart part, Share share)
{
if (!part.AllowCommonLine || part.Quantity < 2)
return 0;
return share == Share.All ? part.Quantity / 2 : part.Quantity / 4;
}
// The second copy turns half a turn from the first, so the part must allow both angles.
private static bool CanTurnHalf(RotationPolicy rotation) =>
rotation.Kind == RotationPolicyKind.Automatic || rotation.Allow180Equivalent;
public IReadOnlyDictionary ReleasedInserts(NestJobResult engineResult) =>
new Dictionary(StringComparer.Ordinal);
public bool TryExpand(NestJob original, NestJobResult result, CancellationToken token,
out NestJobResult? expanded, out string failure)
{
expanded = null;
failure = "Invalid common-line engine accounting";
if (!PipelinePrepassAccounting.Matches(EngineJob, result, token))
return false;
var builder = new NestJobResultBuilder(original);
foreach (var sheet in result.Plates)
{
token.ThrowIfCancellationRequested();
if (sheet.CommonLines.Count > 0)
return false; // Engines place pairs as parts; they never return groups of their own here.
var poses = new List<(string PartId, double X, double Y, double Rotation)>();
var groups = new List();
foreach (var pose in sheet.Placements)
{
if (!proxies.TryGetValue(pose.PartId, out var proxy))
{
poses.Add((pose.PartId, pose.X, pose.Y, pose.Rotation));
continue;
}
var sine = PortableMath.Sin(pose.Rotation);
var cosine = PortableMath.Cos(pose.Rotation);
var pair = proxy.Pair;
groups.Add(new NestJobCommonLine(kerf, new[] { poses.Count, poses.Count + 1 }));
poses.Add((proxy.PartId, pose.X, pose.Y, pose.Rotation));
poses.Add((proxy.PartId,
pose.X + pair.SecondX * cosine - pair.SecondY * sine,
pose.Y + pair.SecondX * sine + pair.SecondY * cosine,
pose.Rotation + CommonLinePair.SecondRotation));
}
var stock = original.Plates.Single(s => s.Id == sheet.StockId);
try
{
builder.AddSheet(stock, poses, groups);
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
return false;
}
}
token.ThrowIfCancellationRequested();
expanded = builder.Build(result.StopReason);
return true;
}
}