using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
///
/// Capture of a plate's common-line groups cut once (docs/common-line-cutting.md). Each group is
/// cut member by member in 's order: a member's holes, the
/// shared cuts it owns (open centerline cuts), then its remainder pieces, which end in kerf those
/// shared cuts, or an earlier member's, opened. The owner of a shared cut therefore precedes its
/// partner. Shared spans are derived from the placed geometry at capture; a broken group refuses.
///
internal sealed class CommonLineCapture
{
private readonly Dictionary members = new(ReferenceEqualityComparer.Instance);
private CommonLineCapture()
{
}
/// Null when the request's plate cuts no common line once.
/// A group is broken.
internal static CommonLineCapture Of(CuttingPlanRequest request)
{
var plate = request?.Plate;
if (plate == null || !plate.CutsCommonLinesOnce)
return null;
var capture = new CommonLineCapture();
foreach (var group in plate.CommonLines)
{
var check = CommonLineGeometry.Check(group);
if (!check.IsIntact)
{
var problem = check.Problems[0];
throw new NotSupportedException(
$"Part {IndexOf(request.Parts, problem.Part) + 1} {problem.Message} Rejoin its common line, "
+ "remove it, or cut the plate's common lines twice, then replan.");
}
var order = CommonLineCutting.Order(group, check);
for (var i = 0; i < order.Members.Count; i++)
{
var part = order.Members[i];
if (IndexOf(request.Parts, part) < 0)
throw new ArgumentException("A common-line member is not among the planned parts.");
capture.members[part] = new Member(order, part, order.SpansOf(part).ToArray(),
order.CutsOwnedBy(part).ToArray());
}
}
return capture;
}
/// Source ordinals of every member, for the rapid checker.
internal IReadOnlySet MemberNumbers(IReadOnlyList parts) =>
members.Keys.Select(part => IndexOf(parts, part) + 1).ToHashSet();
/// (part, prerequisite) source ordinals: each shared cut's owner precedes its partner.
internal IReadOnlyList<(int Part, int Prerequisite)> Order(IReadOnlyList parts) =>
members.Values.SelectMany(member => member.Owned)
.Select(cut => (IndexOf(parts, cut.Partner), IndexOf(parts, cut.Owner)))
.Distinct()
.ToArray();
internal bool IsMember(Part part) => members.ContainsKey(part);
/// The member's owned clean program with its shared spans on the Tab layer.
/// A span does not lie along the program's edges.
internal Program Mark(Part part, Program ownedClean, Vector location, int ordinal)
{
var member = members[part];
try
{
return CommonLineCutting.MarkSharedSpans(ownedClean, location, member.Spans, member.Order.Group.Kerf);
}
catch (ArgumentException exception)
{
throw new NotSupportedException($"Part {ordinal + 1} cannot be cut on its common line: {exception.Message}");
}
}
///
/// The member's shared cuts in part coordinates, each cut from whichever end its straight
/// leads fit: the cut, its lead-in and its lead-out may touch no material, and the pierce keeps
/// the pierce zone. Leads are never shortened: a cut whose leads fit at neither end refuses,
/// naming the span.
///
/// No lead fits a shared cut.
internal CommonLinePerimeter Perimeter(Part part, int ordinal, IReadOnlyList parts,
LeadMaterialSnapshot own, IReadOnlyList materials, CuttingParameters parameters,
double pierceRadius, CancellationToken token)
{
var member = members[part];
var location = part.Location;
var leadIn = parameters.ExternalLeadIn ?? new NoLeadIn();
var leadOut = parameters.ExternalLeadOut ?? new NoLeadOut();
var cuts = new List();
foreach (var cut in member.Owned)
{
token.ThrowIfCancellationRequested();
var forward = new CommonLineCutPath(cut.Span.Start - location, cut.Span.End - location);
var reverse = new CommonLineCutPath(forward.End, forward.Start);
string reason = null;
CommonLineCutPath chosen = null;
foreach (var path in new[] { forward, reverse })
{
var program = new Program(Mode.Absolute);
program.Codes.AddRange(ContourCuttingStrategy.CommonLineSharedCutCodes(path, leadIn, leadOut));
var execution = ExecutionMotionReader.ReadSupported(program, location, null, token);
var result = LeadPathValidator.Check(execution, own, materials, pierceRadius, token);
if (result.IsComplete && result.IsClear)
{
chosen = path;
break;
}
reason ??= result.Reason;
}
if (chosen == null)
throw new NotSupportedException(FormattableString.Invariant(
$"No lead fits the common line between parts {ordinal + 1} and {IndexOf(parts, cut.Partner) + 1} from ({cut.Span.Start.X:0.####}, {cut.Span.Start.Y:0.####}) to ({cut.Span.End.X:0.####}, {cut.Span.End.Y:0.####}) at either end: ")
+ reason + " Shorten the external lead-in or lead-out, or cut the plate's common lines twice.");
cuts.Add(chosen);
}
return new CommonLinePerimeter(member.Order.Group.Kerf, cuts);
}
private static int IndexOf(IReadOnlyList parts, Part part)
{
for (var i = 0; i < parts.Count; i++)
if (ReferenceEquals(parts[i], part))
return i;
return -1;
}
private sealed record Member(CommonLineCutOrder Order, Part Part, IReadOnlyList Spans,
IReadOnlyList Owned);
}