using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
{
/// The outcome of : the group made, or why not.
public sealed record CommonLineJoinResult(CommonLineGroup Group, string Failure)
{
public bool Succeeded => Group != null;
}
///
/// Slides parts together one kerf apart along their best facing straight edges and groups them
/// for common-line cutting. Parts already in a group move with their whole group, which joins
/// the new one. Nothing moves unless every part finds a partner and the result is an intact group.
///
public static class CommonLineJoiner
{
/// How far apart facing edges may start (or overlap) and still be joined.
public static CommonLineJoinResult Join(Plate plate, IReadOnlyList parts, double kerf, double maxGap)
{
ArgumentNullException.ThrowIfNull(plate);
ArgumentNullException.ThrowIfNull(parts);
if (!double.IsFinite(kerf) || kerf <= 0)
return new(null, "The plate has no kerf for its material and thickness, so parts cannot share a common line.");
if (!double.IsFinite(maxGap) || maxGap < kerf)
maxGap = kerf;
var selected = parts.Where(part => part != null && !part.BaseDrawing.IsCutOff).Distinct().ToList();
if (selected.Any(part => !plate.Parts.Contains(part)))
return new(null, "Every part in a common line must be on the plate.");
// A part already in a group brings its whole group, which moves as one unit.
var units = new List>();
foreach (var part in selected)
{
if (units.Any(unit => unit.Contains(part)))
continue;
units.Add(plate.CommonLineGroupOf(part) is { } group ? group.Members.ToList() : new List { part });
}
if (units.Sum(unit => unit.Count) < 2)
return new(null, "Select at least two parts to share a common line.");
var start = units.SelectMany(unit => unit).ToDictionary(part => part, part => part.Location);
// Anchor the unit nearest the plate origin; join the rest to whatever is placed.
var placed = units.OrderBy(unit => unit.Min(part => part.BoundingBox.Left + part.BoundingBox.Bottom)).First();
var anchored = new List(placed);
var remaining = units.Where(unit => unit != placed).ToList();
while (remaining.Count > 0)
{
var best = remaining
.SelectMany(unit => unit.SelectMany(candidate => anchored.Select(anchor =>
(Unit: unit, Offset: CommonLineGeometry.JoinOffset(anchor, candidate, kerf, maxGap)))))
.Where(choice => choice.Offset != null)
.OrderBy(choice => choice.Offset!.Value.DistanceTo(Vector.Zero))
.FirstOrDefault();
if (best.Unit == null)
{
Restore(start);
var names = string.Join(", ", remaining.SelectMany(unit => unit).Select(part => part.BaseDrawing.Name).Distinct());
return new(null, $"No straight edge of {names} faces another selected part within {maxGap:0.###}. "
+ "Place the parts edge to edge first.");
}
foreach (var part in best.Unit)
part.Offset(best.Offset!.Value);
anchored.AddRange(best.Unit);
remaining.Remove(best.Unit);
}
var trial = new CommonLineGroup(anchored, kerf);
var check = CommonLineGeometry.Check(trial);
if (!check.IsIntact)
{
Restore(start);
return new(null, $"'{check.Problems[0].Part.BaseDrawing.Name}' {check.Problems[0].Message}");
}
return new(plate.JoinCommonLine(anchored, kerf), null);
}
private static void Restore(Dictionary start)
{
foreach (var (part, location) in start)
if (part.Location != location)
part.Offset(location - part.Location);
}
}
}