using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest
{
///
/// One shared cut of a common-line group, cut once on the centerline by , the
/// member of the pair cut first. is cut later; its remainder ends in the
/// kerf this cut opened.
///
public sealed record CommonLineSharedCut(CommonLineSpan Span, Part Owner, Part Partner);
///
/// How a common-line group is cut once (see docs/common-line-cutting.md): its members in cutting
/// order, and each shared cut with the member that cuts it.
///
public sealed record CommonLineCutOrder(CommonLineGroup Group, IReadOnlyList Members,
IReadOnlyList Cuts)
{
/// The shared cuts cuts.
public IEnumerable CutsOwnedBy(Part member) =>
Cuts.Where(cut => ReferenceEquals(cut.Owner, member));
/// Every span shares, whoever cuts it.
public IEnumerable SpansOf(Part member) => Cuts
.Where(cut => ReferenceEquals(cut.Owner, member) || ReferenceEquals(cut.Partner, member))
.Select(cut => cut.Span);
}
///
/// Single-pass common-line cutting: which member cuts each shared span, and the planner's owned
/// clean perimeter with its shared spans marked as never cut by that member.
///
public static class CommonLineCutting
{
///
/// The cutting order of an intact group: members breadth-first from its first member along
/// shared spans (ties in member order), each shared cut owned by the member cut first. A
/// member's remainder therefore only ever ends in kerf cut before it.
///
/// The group is not intact.
public static CommonLineCutOrder Order(CommonLineGroup group, CommonLineCheck check)
{
ArgumentNullException.ThrowIfNull(group);
ArgumentNullException.ThrowIfNull(check);
if (!check.IsIntact)
throw new ArgumentException(check.Problems[0].Message, nameof(check));
var members = group.Members;
int IndexOf(Part part) => members.ToList().FindIndex(member => ReferenceEquals(member, part));
var order = new List();
var seen = new bool[members.Count];
var queue = new Queue();
queue.Enqueue(0);
seen[0] = true;
while (queue.Count > 0)
{
var current = members[queue.Dequeue()];
order.Add(current);
var neighbors = check.Spans
.Select(span => ReferenceEquals(span.First, current) ? span.Second
: ReferenceEquals(span.Second, current) ? span.First : null)
.Where(part => part != null)
.Select(IndexOf)
.Distinct()
.Order();
foreach (var next in neighbors)
if (!seen[next])
{
seen[next] = true;
queue.Enqueue(next);
}
}
if (order.Count != members.Count)
throw new ArgumentException("The common-line group has split into separate pieces.", nameof(check));
int Rank(Part part) => order.FindIndex(member => ReferenceEquals(member, part));
var cuts = check.Spans
.Select(span => Rank(span.First) < Rank(span.Second)
? new CommonLineSharedCut(span, span.First, span.Second)
: new CommonLineSharedCut(span, span.Second, span.First))
.OrderBy(cut => Rank(cut.Owner))
.ToList();
return new CommonLineCutOrder(group, order, cuts);
}
///
/// A copy of (a member's clean program, rotated as placed, in part
/// coordinates; placed at ) whose perimeter edges are split at
/// every end of and whose shared portions are on the
/// layer: material that this member never cuts, because the
/// shared cut does. The result is absolute. Only straight top-level moves can share.
///
/// A span does not lie along the program's edges.
public static Program MarkSharedSpans(Program clean, Vector location, IReadOnlyList spans,
double kerf)
{
ArgumentNullException.ThrowIfNull(clean);
ArgumentNullException.ThrowIfNull(spans);
if (!double.IsFinite(kerf) || kerf <= 0)
throw new ArgumentOutOfRangeException(nameof(kerf));
var program = (Program)clean.Clone();
program.Mode = Mode.Absolute;
var local = spans.Select(span => (Start: span.Start - location, End: span.End - location)).ToArray();
var covered = new double[local.Length];
var codes = new List();
Vector? position = null;
foreach (var code in program.Codes)
{
if (code is LinearMove line && position is { } start
&& line.Layer is LayerType.Cut or LayerType.Display)
{
codes.AddRange(Split(line, start, local, covered, kerf));
position = line.EndPoint;
continue;
}
codes.Add(code);
position = code switch
{
Motion motion => motion.EndPoint,
SubProgramCall => null,
_ => position,
};
}
for (var i = 0; i < local.Length; i++)
if (System.Math.Abs(covered[i] - local[i].Start.DistanceTo(local[i].End)) > CommonLineGeometry.GapTolerance)
throw new ArgumentException(FormattableString.Invariant(
$"The shared span from ({spans[i].Start.X:0.####}, {spans[i].Start.Y:0.####}) to ({spans[i].End.X:0.####}, {spans[i].End.Y:0.####}) does not lie along one straight edge of the part's program."));
program.Codes.Clear();
program.Codes.AddRange(codes);
return program;
}
// The line split where shared spans one half kerf off it begin and end; shared pieces on Tab.
private static IEnumerable Split(LinearMove line, Vector start, (Vector Start, Vector End)[] spans,
double[] covered, double kerf)
{
var end = line.EndPoint;
var length = start.DistanceTo(end);
if (length <= Math.Tolerance.Epsilon)
return [line];
var direction = new Vector((end.X - start.X) / length, (end.Y - start.Y) / length);
var intervals = new List<(double From, double To)>();
for (var i = 0; i < spans.Length; i++)
{
var (a, b) = spans[i];
var spanLength = a.DistanceTo(b);
if (spanLength <= Math.Tolerance.Epsilon)
continue;
var along = new Vector((b.X - a.X) / spanLength, (b.Y - a.Y) / spanLength);
if (System.Math.Abs(Cross(direction, along)) > 1e-6)
continue;
var offset = System.Math.Abs(Cross(direction, a - start));
if (System.Math.Abs(offset - kerf / 2) > CommonLineGeometry.GapTolerance)
continue;
var ta = Dot(a - start, direction);
var tb = Dot(b - start, direction);
var from = System.Math.Max(0, System.Math.Min(ta, tb));
var to = System.Math.Min(length, System.Math.Max(ta, tb));
if (to - from <= Math.Tolerance.Epsilon)
continue;
// An end within the snap tolerance of a vertex is that vertex: no sliver is left uncut.
if (from <= Math.Tolerance.Epsilon)
from = 0;
if (length - to <= Math.Tolerance.Epsilon)
to = length;
covered[i] += to - from;
intervals.Add((from, to));
}
if (intervals.Count == 0)
return [line];
var pieces = new List();
var at = 0.0;
foreach (var (from, to) in intervals.OrderBy(interval => interval.From))
{
if (from < at - Math.Tolerance.Epsilon)
throw new ArgumentException("Shared spans overlap along one edge.");
if (from - at > Math.Tolerance.Epsilon)
pieces.Add(new LinearMove(Point(from)) { Layer = line.Layer });
pieces.Add(new LinearMove(to >= length ? end : Point(to)) { Layer = LayerType.Tab });
at = to;
}
if (length - at > Math.Tolerance.Epsilon)
pieces.Add(new LinearMove(end) { Layer = line.Layer });
else if (pieces[^1] is LinearMove last)
last.EndPoint = end;
return pieces;
Vector Point(double t) => new(start.X + direction.X * t, start.Y + direction.Y * t);
}
private static double Dot(Vector a, Vector b) => a.X * b.X + a.Y * b.Y;
private static double Cross(Vector a, Vector b) => a.X * b.Y - a.Y * b.X;
}
}