using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest
{
/// One shared cut: the centerline between two parts' facing edges, one kerf apart.
public readonly record struct CommonLineSpan(Part First, Part Second, Vector Start, Vector End)
{
public double Length => Start.DistanceTo(End);
}
/// A member of a group that no longer shares a span, or could not be read.
public readonly record struct CommonLineProblem(Part Part, string Message);
/// Spans a group's members share now, and what breaks the group.
public sealed record CommonLineCheck(IReadOnlyList Spans, IReadOnlyList Problems)
{
public bool IsIntact => Problems.Count == 0;
}
///
/// Derives common-line spans from placed parts' clean perimeters (leads excluded, holes never
/// share). Two straight perimeter edges share a span when they face each other (antiparallel,
/// outward normals opposed), lie one kerf apart within and overlap
/// along the line by at least . Arcs never share.
///
public static class CommonLineGeometry
{
/// How far a shared edge pair's gap may differ from the group's kerf (inches).
public const double GapTolerance = 0.0005;
/// The shortest overlap that counts as a shared span (inches).
public const double MinSharedLength = 0.25;
private const double ParallelTolerance = 1e-6;
/// A placed perimeter edge with its outward unit normal.
internal readonly record struct Edge(Vector Start, Vector End, Vector Direction, Vector Outward, double Length);
/// A facing edge pair: along the first edge's outward normal,
/// the overlap as distances along the first edge from its start.
internal readonly record struct Facing(Edge First, Edge Second, double Gap, double From, double To)
{
public double Overlap => To - From;
}
/// The spans and share at .
public static IReadOnlyList Spans(Part first, Part second, double kerf)
{
ArgumentNullException.ThrowIfNull(first);
ArgumentNullException.ThrowIfNull(second);
var cache = new ProfileCache();
return Spans(first, Edges(first, cache), second, Edges(second, cache), kerf);
}
///
/// Checks that every member of shares at least one span with another
/// member at the group's kerf, and that the members form one connected group.
///
public static CommonLineCheck Check(CommonLineGroup group)
{
ArgumentNullException.ThrowIfNull(group);
var members = group.Members;
var cache = new ProfileCache();
var edges = new IReadOnlyList[members.Count];
var problems = new List();
for (var i = 0; i < members.Count; i++)
{
try
{
edges[i] = Edges(members[i], cache);
}
catch (Exception exception) when (IsInvalid(exception))
{
problems.Add(new(members[i], $"could not be read ({exception.Message})."));
}
}
var spans = new List();
var links = members.Select(_ => new List()).ToArray();
for (var i = 0; i < members.Count; i++)
for (var j = i + 1; j < members.Count; j++)
{
if (edges[i] == null || edges[j] == null)
continue;
var shared = Spans(members[i], edges[i], members[j], edges[j], group.Kerf);
if (shared.Count == 0)
continue;
spans.AddRange(shared);
links[i].Add(j);
links[j].Add(i);
}
for (var i = 0; i < members.Count; i++)
if (edges[i] != null && links[i].Count == 0)
problems.Add(new(members[i],
$"no longer shares a straight edge one kerf ({group.Kerf:0.####}) apart with another part in its common-line group."));
if (problems.Count == 0 && !Connected(links))
problems.Add(new(members[0], "its common-line group has split into separate pieces."));
return new CommonLineCheck(spans, problems);
}
///
/// The move that puts one kerf from along
/// their best facing edge pair: the longest overlap, then the smallest gap. Only pairs closer
/// than (or already overlapping by up to that much) are considered;
/// null when there are none.
///
public static Vector? JoinOffset(Part anchor, Part moving, double kerf, double maxGap)
{
ArgumentNullException.ThrowIfNull(anchor);
ArgumentNullException.ThrowIfNull(moving);
if (!double.IsFinite(kerf) || kerf <= 0)
throw new ArgumentOutOfRangeException(nameof(kerf));
var cache = new ProfileCache();
var best = FacingEdges(Edges(anchor, cache), Edges(moving, cache), -maxGap, maxGap)
.OrderByDescending(facing => System.Math.Round(facing.Overlap, 6))
.ThenBy(facing => System.Math.Abs(facing.Gap - kerf))
.Cast()
.FirstOrDefault();
if (best is not { } chosen)
return null;
var shift = kerf - chosen.Gap;
return new Vector(chosen.First.Outward.X * shift, chosen.First.Outward.Y * shift);
}
private static IReadOnlyList Spans(Part first, IReadOnlyList a, Part second,
IReadOnlyList b, double kerf)
{
if (!double.IsFinite(kerf) || kerf <= 0)
throw new ArgumentOutOfRangeException(nameof(kerf));
var spans = new List();
foreach (var facing in FacingEdges(a, b, kerf - GapTolerance, kerf + GapTolerance))
{
var edge = facing.First;
var half = facing.Gap / 2;
Vector At(double t) => new(edge.Start.X + edge.Direction.X * t + edge.Outward.X * half,
edge.Start.Y + edge.Direction.Y * t + edge.Outward.Y * half);
spans.Add(new CommonLineSpan(first, second, At(facing.From), At(facing.To)));
}
return spans;
}
internal static IEnumerable FacingEdges(IReadOnlyList a, IReadOnlyList b,
double minGap, double maxGap)
{
foreach (var first in a)
foreach (var second in b)
{
// Antiparallel edges with opposed outward normals face each other.
if (System.Math.Abs(Cross(first.Direction, second.Direction)) > ParallelTolerance
|| Dot(first.Outward, second.Outward) > -1 + ParallelTolerance)
continue;
var gap = Dot(second.Start - first.Start, first.Outward);
if (gap < minGap || gap > maxGap)
continue;
var t0 = Dot(second.Start - first.Start, first.Direction);
var t1 = Dot(second.End - first.Start, first.Direction);
var from = System.Math.Max(0, System.Math.Min(t0, t1));
var to = System.Math.Min(first.Length, System.Math.Max(t0, t1));
if (to - from >= MinSharedLength)
yield return new Facing(first, second, gap, from, to);
}
}
/// The straight edges of a part's placed clean perimeter, with outward normals.
internal static IReadOnlyList Edges(Part part, ProfileCache cache)
{
var profile = PartSpacingReview.Profile(part, cache.Profiles)
?? throw new ArgumentException("Part has no material.");
var location = part.Location;
var ring = profile.Perimeter.ToPolygon();
var edges = new List();
foreach (var line in profile.Perimeter.Entities.OfType())
{
var start = line.StartPoint + location;
var end = line.EndPoint + location;
var length = start.DistanceTo(end);
if (length < MinSharedLength)
continue;
var direction = new Vector((end.X - start.X) / length, (end.Y - start.Y) / length);
var outward = new Vector(direction.Y, -direction.X);
// The probe is in part-local coordinates: the ring is not moved to the location.
var mid = new Vector((line.StartPoint.X + line.EndPoint.X) / 2, (line.StartPoint.Y + line.EndPoint.Y) / 2);
var step = System.Math.Min(0.001, length / 100);
if (Inside(ring.Vertices, new Vector(mid.X + outward.X * step, mid.Y + outward.Y * step)))
outward = new Vector(-outward.X, -outward.Y);
edges.Add(new Edge(start, end, direction, outward, length));
}
return edges;
}
internal sealed class ProfileCache
{
public readonly Dictionary<(Program, long), ShapeProfile> Profiles = new();
}
private static bool Connected(IReadOnlyList> links)
{
var seen = new bool[links.Count];
var stack = new Stack();
stack.Push(0);
seen[0] = true;
while (stack.Count > 0)
foreach (var next in links[stack.Pop()])
if (!seen[next])
{
seen[next] = true;
stack.Push(next);
}
return seen.All(s => s);
}
private static bool Inside(IReadOnlyList ring, Vector point)
{
var inside = false;
for (int i = 0, j = ring.Count - 1; i < ring.Count; j = i++)
{
var a = ring[i];
var b = ring[j];
if ((a.Y > point.Y) != (b.Y > point.Y)
&& point.X < (b.X - a.X) * (point.Y - a.Y) / (b.Y - a.Y) + a.X)
inside = !inside;
}
return inside;
}
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;
private static bool IsInvalid(Exception exception) => exception is
ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException;
}
}