using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.CuttingPlanning;
///
/// Whole-part cutting prerequisites for one captured plate. A cutoff precedes every part its
/// nominal span crosses (an orphaned cutoff, whose definition is missing, precedes every part),
/// a part proven to lie inside a cutout of another part precedes that host, and a cut-first part
/// precedes every other part that need not precede it. Built on the
/// caller thread from owned values; ambiguous containment refuses instead of guessing from bounds.
///
internal sealed class CuttingDependencyGraph
{
private readonly int[][] prerequisites;
private CuttingDependencyGraph(int[][] prerequisites) => this.prerequisites = prerequisites;
internal static CuttingDependencyGraph Empty(int count) =>
new(Enumerable.Range(0, count).Select(_ => Array.Empty()).ToArray());
/// Exact prerequisites, for tests and callers that already proved them.
internal static CuttingDependencyGraph FromPrerequisites(IReadOnlyList prerequisites)
{
var copy = prerequisites.Select(p => p.Distinct().Order().ToArray()).ToArray();
if (copy.SelectMany(p => p).Any(i => i < 0 || i >= copy.Length))
throw new ArgumentException("Prerequisite ordinal out of range.");
return new(copy);
}
internal int Count => prerequisites.Length;
internal IReadOnlyList PrerequisitesOf(int ordinal) => prerequisites[ordinal];
internal bool IsReady(int ordinal, IEnumerable done)
{
if (prerequisites[ordinal].Length == 0)
return true;
var finished = done as ICollection ?? done.ToHashSet();
return prerequisites[ordinal].All(finished.Contains);
}
/// First (part, missing prerequisite) in a proposed complete order, or null.
internal (int Part, int Prerequisite)? FirstViolation(IReadOnlyList order)
{
var position = Enumerable.Repeat(-1, prerequisites.Length).ToArray();
for (var i = 0; i < order.Count; i++)
position[order[i]] = i;
foreach (var part in order)
foreach (var prerequisite in prerequisites[part])
if (position[prerequisite] < 0 || position[prerequisite] > position[part])
return (part, prerequisite);
return null;
}
/// An ordinal on a prerequisite cycle, or null when the graph is acyclic.
internal int? FindCycle()
{
var remaining = prerequisites.Select(p => p.Length).ToArray();
var dependents = Enumerable.Range(0, prerequisites.Length).Select(_ => new List()).ToArray();
for (var part = 0; part < prerequisites.Length; part++)
foreach (var prerequisite in prerequisites[part])
dependents[prerequisite].Add(part);
var ready = new Queue(Enumerable.Range(0, prerequisites.Length).Where(i => remaining[i] == 0));
while (ready.Count != 0)
foreach (var dependent in dependents[ready.Dequeue()])
if (--remaining[dependent] == 0)
ready.Enqueue(dependent);
var cyclic = Array.FindIndex(remaining, r => r != 0);
return cyclic < 0 ? null : cyclic;
}
/// Further (part, prerequisite) ordinals the caller proved, such as a
/// common line's owner before its partner.
internal static CuttingDependencyGraph Build(IReadOnlyList nodes, Box plate,
CancellationToken token, Action uncertain = null,
IReadOnlyList<(int Part, int Prerequisite)> required = null)
{
var edges = nodes.Select(_ => new SortedSet()).ToArray();
foreach (var (part, prerequisite) in required ?? [])
edges[part].Add(prerequisite);
for (var cut = 0; cut < nodes.Count; cut++)
{
if (!nodes[cut].IsCutOff)
continue;
if (nodes[cut].SeparatesSheet)
{
// Cutting a line that splits the sheet comes after every part and every other
// cutoff, so the head never travels back across the separated edge and the
// remnant's mass stays on to the end. Among separators, a line stopping on others
// (a later round) goes before them. Such a line is continuous, so it passes
// through no part and needs no crossing edge.
for (var other = 0; other < nodes.Count; other++)
if (!nodes[other].SeparatesSheet || nodes[other].SeparatorRound > nodes[cut].SeparatorRound)
edges[cut].Add(other);
continue;
}
var definition = nodes[cut].CutOff;
for (var part = 0; part < nodes.Count; part++)
{
token.ThrowIfCancellationRequested();
if (!nodes[part].IsCutOff && (definition == null || CutOffCrosses(definition.Axis,
definition.Position, definition.StartLimit, definition.EndLimit, nodes[part].PlacedBounds, plate,
definition.Gaps)))
edges[part].Add(cut);
}
}
for (var inner = 0; inner < nodes.Count; inner++)
for (var host = 0; host < nodes.Count; host++)
{
token.ThrowIfCancellationRequested();
// Bounds only select candidates; containment itself is proven on native material.
if (inner != host && !nodes[inner].IsCutOff && !nodes[host].IsCutOff
&& BoundingBox.IsWithin(nodes[inner].CleanBounds, nodes[host].HostBounds))
{
try
{
if (InsideCutout(nodes, inner, host, token))
edges[host].Add(inner);
}
catch (CuttingDependencyException exception) when (uncertain != null
&& exception.Status == CuttingPlanStatus.UnsupportedGeometry)
{
uncertain(exception);
}
catch (Exception exception) when (uncertain != null
&& exception is ArgumentException or NotSupportedException)
{
uncertain(Ambiguous(inner, host, exception.Message));
}
}
}
// Cut-first parts (test coupons) precede every other part, except one that must already
// precede the coupon (nested in its cutout): containment wins over the preference.
for (var first = 0; first < nodes.Count; first++)
{
if (!nodes[first].CutFirst)
continue;
var before = Ancestors(edges, first);
for (var part = 0; part < nodes.Count; part++)
if (!nodes[part].IsCutOff && !nodes[part].CutFirst && !before.Contains(part))
edges[part].Add(first);
}
var graph = new CuttingDependencyGraph(edges.Select(e => e.ToArray()).ToArray());
if (graph.FindCycle() is { } cyclic)
throw new CuttingDependencyException(CuttingPlanStatus.ConstraintConflict, cyclic, null,
$"Part {cyclic + 1} is on a cutting dependency cycle.");
return graph;
}
// Every ordinal that must come before the given one, directly or transitively.
private static HashSet Ancestors(IReadOnlyList> edges, int ordinal)
{
var found = new HashSet();
var pending = new Stack(edges[ordinal]);
while (pending.Count != 0)
{
var next = pending.Pop();
if (found.Add(next))
foreach (var prerequisite in edges[next])
pending.Push(prerequisite);
}
return found;
}
///
/// Whether a cutoff's nominal line crosses a part's bounds within the cutoff's span. Uses the
/// nominal line, not its trimmed cutting segments (which deliberately skip the parts). Bounds
/// conservatively include edge contacts and concave recesses; limits prevent unrelated
/// dependencies beyond the cutoff's span. A part wholly inside one of the line's
/// is not crossed. Negative coordinates need no special case.
///
internal static bool CutOffCrosses(CutOffAxis axis, Vector cutOffPosition, double? startLimit,
double? endLimit, Box part, Box plate, IReadOnlyList gaps = null)
{
var vertical = axis == CutOffAxis.Vertical;
var position = vertical ? cutOffPosition.X : cutOffPosition.Y;
var acrossMin = vertical ? part.Left : part.Bottom;
var acrossMax = vertical ? part.Right : part.Top;
var alongMin = vertical ? part.Bottom : part.Left;
var alongMax = vertical ? part.Top : part.Right;
var start = startLimit ?? (vertical ? plate.Bottom : plate.Left);
var end = endLimit ?? (vertical ? plate.Top : plate.Right);
if (position < acrossMin - Tolerance.Epsilon || position > acrossMax + Tolerance.Epsilon)
return false;
return CutOff.Pieces(start, end, gaps ?? Array.Empty()).Any(piece =>
!(piece.End < alongMin - Tolerance.Epsilon || piece.Start > alongMax + Tolerance.Epsilon));
}
// True when the inner part's perimeter lies strictly inside one cutout of the host. Any
// boundary contact or crossing between them, or incomplete material, is ambiguous.
private static bool InsideCutout(IReadOnlyList nodes, int inner, int host,
CancellationToken token) =>
NestedPartContainment.InsideCutout(nodes[inner].Material, nodes[host].Material, token, out var ambiguity)
?? throw Ambiguous(inner, host, ambiguity);
private static CuttingDependencyException Ambiguous(int inner, int host, string reason) =>
new(CuttingPlanStatus.UnsupportedGeometry, inner, host,
$"Containment of part {inner + 1} relative to part {host + 1} cannot be established: {reason}.");
}
/// Owned copy of a cutoff definition's nominal line.
internal sealed record CutOffDefinition(CutOffAxis Axis, Vector Position, double? StartLimit, double? EndLimit,
IReadOnlyList Gaps);
/// Owned per-placement dependency input; material is captured only when a pair needs it.
internal sealed class DependencyNode
{
private readonly Func materialFactory;
private LeadMaterialSnapshot material;
internal DependencyNode(bool isCutOff, CutOffDefinition cutOff, Box placedBounds, Box cleanBounds,
Func materialFactory, int? separatorRound = null, bool cutFirst = false)
{
IsCutOff = isCutOff;
CutFirst = !isCutOff && cutFirst;
SeparatorRound = isCutOff ? separatorRound : null;
CutOff = cutOff;
PlacedBounds = Copy(placedBounds);
CleanBounds = cleanBounds == null ? null : Copy(cleanBounds);
HostBounds = cleanBounds == null
? PlacedBounds
: BoundingBox.Union(PlacedBounds, CleanBounds);
this.materialFactory = materialFactory;
}
internal bool IsCutOff { get; }
///
/// A cutoff whose placed program is one continuous cut splitting off a piece of the sheet,
/// alone or with the separators it stops on (see CutOff.Separators).
///
internal bool SeparatesSheet => SeparatorRound != null;
/// The separator's round: 0 across the whole sheet, higher for lines stopping on earlier rounds.
internal int? SeparatorRound { get; }
/// A part whose drawing asks to be cut before the plate's other parts.
internal bool CutFirst { get; }
internal CutOffDefinition CutOff { get; }
internal Box PlacedBounds { get; }
internal Box CleanBounds { get; }
internal Box HostBounds { get; }
internal LeadMaterialSnapshot Material => material ??= materialFactory();
private static Box Copy(Box box) => new(box.X, box.Y, box.Length, box.Width);
}
/// A dependency refusal naming the part and, when relevant, the other part.
internal sealed class CuttingDependencyException(CuttingPlanStatus status, int ordinal, int? other, string message)
: Exception(message)
{
internal CuttingPlanStatus Status { get; } = status;
internal int Ordinal { get; } = ordinal;
internal int? Other { get; } = other;
}