Files
OpenNest/OpenNest.Core/CommonLineJoiner.cs
T
ajandClaude Opus 5.5 1d87d212de feat(common-line): place parts one kerf apart to share an edge
Expensive alloy jobs (N26-295, 310 stainless) could not fit their last
PT42 at full part spacing; the plate was squeezed to 0.2 spacing with a
part 0.14 from the edge. Parts whose straight edges face each other one
kerf apart are cut on the same centerline, saving the spacing along each
shared edge.

Plate.CommonLines holds groups of parts and the kerf they were joined
at. Shared spans are derived from the placed clean perimeters, never
stored, so a moved member breaks its group instead of leaving a stale
span. Plate > Arrange > Join Common Line slides the selected parts
together at the plate's kerf; Remove Common Line ungroups them. The
plate draws shared spans as a dashed centerline.

Both spacing checks hold group members to the kerf instead of the part
spacing, and a new pre-post Common lines check reports broken groups, a
changed kerf, and leads that run into a partner. Groups persist as part
positions in an optional commonLines array that older readers ignore.

This first slice still cuts every shared edge twice; single-pass cutting
through Plan Cutting and the posts comes next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:16:13 -05:00

89 lines
4.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
{
/// <summary>The outcome of <see cref="CommonLineJoiner.Join"/>: the group made, or why not.</summary>
public sealed record CommonLineJoinResult(CommonLineGroup Group, string Failure)
{
public bool Succeeded => Group != null;
}
/// <summary>
/// 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.
/// </summary>
public static class CommonLineJoiner
{
/// <param name="maxGap">How far apart facing edges may start (or overlap) and still be joined.</param>
public static CommonLineJoinResult Join(Plate plate, IReadOnlyList<Part> 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<List<Part>>();
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> { 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<Part>(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<Part, Vector> start)
{
foreach (var (part, location) in start)
if (part.Location != location)
part.Offset(location - part.Location);
}
}
}