mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-09 11:05:43 -05:00
The part cut first no longer makes a separate uncompensated centerline cut with its own lead-in and lead-out, then re-pierces for its remainder. It cuts its outside compensated, which puts its shared edge on the kerf centerline, starting on the common line: a straight lead-in from the scrap along the shared edge, then the line and on round its perimeter with no lead-out. A member with spans already cut stops in that kerf and cuts its last stretch from it, ending beside the part it is cut before. Partners still cut only their remainders. Replay's tabbed-ring accounting now accepts pieces that tile each span exactly once, and the pre-post review counts a span as cut by a member's compensated edge (or an older centerline cut). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
10 KiB
C#
203 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// Pre-post review of a plate's common-line groups (<see cref="Plate.CommonLines"/>): every member
|
|
/// still shares a span one kerf from another member, the group's kerf is still the plate's, and no
|
|
/// member's lead or shared cut runs into a partner one kerf away (a lead on a shared edge would cut
|
|
/// the partner). A plate that cuts its common lines once must cut each shared span exactly once, by
|
|
/// the member cut first (its own compensated edge, or a centerline cut in older programs); one set
|
|
/// to cut them twice must have no single-pass member programs.
|
|
/// Reports only; nothing is repaired, and an unreadable lead check is incomplete, never clear.
|
|
/// </summary>
|
|
internal static class CommonLineReview
|
|
{
|
|
internal static void Add(Plate plate, int plateNumber, List<PostVerificationFinding> findings,
|
|
CancellationToken token)
|
|
{
|
|
if (plate.CommonLines is not { Count: > 0 } groups)
|
|
return;
|
|
var plateKerf = plate.CuttingParameters?.Kerf is double k && double.IsFinite(k) && k > 0 ? k : (double?)null;
|
|
foreach (var group in groups)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var first = Number(plate, group.Members[0]);
|
|
if (plateKerf is { } kerf && System.Math.Abs(kerf - group.Kerf) > CommonLineGeometry.GapTolerance)
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, null,
|
|
$"Common line was placed for a {group.Kerf:0.####} kerf, but the plate now cuts with a "
|
|
+ $"{kerf:0.####} kerf. Rejoin the parts at the current kerf before posting."));
|
|
var check = CommonLineGeometry.Check(group);
|
|
foreach (var problem in check.Problems)
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, problem.Part), null,
|
|
$"'{problem.Part.BaseDrawing.Name}' {problem.Message}")
|
|
{ Location = problem.Part.BoundingBox?.Center });
|
|
Leads(plate, plateNumber, group, findings, token);
|
|
if (check.IsIntact)
|
|
SharedCuts(plate, plateNumber, group, check, findings, token);
|
|
}
|
|
}
|
|
|
|
// The shared cuts the members' programs make, against the spans the group derives now.
|
|
private static void SharedCuts(Plate plate, int plateNumber, CommonLineGroup group, CommonLineCheck check,
|
|
List<PostVerificationFinding> findings, CancellationToken token)
|
|
{
|
|
var cuts = new List<(Part Part, ExecutionMotion Motion)>();
|
|
Part singlePass = null;
|
|
foreach (var member in group.Members)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (singlePass == null && CommonLineCuts.IsSinglePass(member.Program))
|
|
singlePass = member;
|
|
try
|
|
{
|
|
cuts.AddRange(ExecutionMotionReader.Read(member.Program, member.Location, null, token).Motions
|
|
.Where(motion => !motion.Rapid && motion.Curve != null
|
|
&& motion.Layer is LayerType.CommonLine or LayerType.Cut or LayerType.Display)
|
|
.Select(motion => (member, motion)));
|
|
}
|
|
catch (Exception exception) when (IsInvalid(exception))
|
|
{
|
|
// The rapid check reports the unreadable program.
|
|
}
|
|
}
|
|
if (plate.CutCommonLinesTwice)
|
|
{
|
|
if (singlePass != null)
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, singlePass), null,
|
|
"The plate is set to cut its common lines twice, but this part's program cuts a shared line "
|
|
+ "once. Plan cutting again before posting.")
|
|
{ Location = singlePass.BoundingBox?.Center });
|
|
return;
|
|
}
|
|
foreach (var span in check.Spans)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var covering = cuts.Where(cut => Covers(cut.Motion, span, group.Kerf)).ToList();
|
|
var first = Number(plate, span.First);
|
|
var second = Number(plate, span.Second);
|
|
var middle = (span.Start + span.End) * 0.5;
|
|
if (covering.Count == 0)
|
|
{
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, second,
|
|
"This common line is not cut. Plan cutting cuts each shared line once; "
|
|
+ "or set the plate to cut its common lines twice.")
|
|
{ Location = middle });
|
|
continue;
|
|
}
|
|
if (covering.Count > 1)
|
|
{
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, second,
|
|
"This common line is cut more than once (a plate not planned since the parts were joined cuts "
|
|
+ "it twice). Plan cutting again before posting, or set the plate to cut its common lines twice.")
|
|
{ Location = middle });
|
|
continue;
|
|
}
|
|
// The partner's remainder ends in this kerf, so the part that cuts it must come first.
|
|
var owner = covering[0].Part;
|
|
var partner = ReferenceEquals(owner, span.First) ? span.Second : span.First;
|
|
if (Number(plate, owner) is int o && Number(plate, partner) is int p && o > p)
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, o, p,
|
|
"The shared line is cut after the partner whose remainder ends in its kerf. Plan cutting "
|
|
+ "again, or move the part that cuts it ahead of its partner.")
|
|
{ Location = middle });
|
|
}
|
|
}
|
|
|
|
// A straight cut covering the whole span: a member's own compensated edge half a kerf off its
|
|
// centerline, or a centerline cut made without compensation.
|
|
private static bool Covers(ExecutionMotion motion, CommonLineSpan span, double kerf)
|
|
{
|
|
var curve = motion.Curve;
|
|
if (curve.IsFiniteArc)
|
|
return false;
|
|
var length = span.Length;
|
|
var along = (span.End - span.Start) * (1 / length);
|
|
double Offset(Vector point) => (point.X - span.Start.X) * along.Y - (point.Y - span.Start.Y) * along.X;
|
|
double At(Vector point) => (point.X - span.Start.X) * along.X + (point.Y - span.Start.Y) * along.Y;
|
|
var tolerance = CommonLineGeometry.GapTolerance;
|
|
var offset = Offset(curve.Start);
|
|
if (System.Math.Abs(Offset(curve.End) - offset) > tolerance)
|
|
return false;
|
|
var expected = motion.Layer == LayerType.CommonLine ? 0 : kerf / 2;
|
|
if (System.Math.Abs(System.Math.Abs(offset) - expected) > tolerance)
|
|
return false;
|
|
var from = System.Math.Min(At(curve.Start), At(curve.End));
|
|
var to = System.Math.Max(At(curve.Start), At(curve.End));
|
|
return from <= tolerance && to >= length - tolerance;
|
|
}
|
|
|
|
private static void Leads(Plate plate, int plateNumber, CommonLineGroup group,
|
|
List<PostVerificationFinding> findings, CancellationToken token)
|
|
{
|
|
var members = group.Members;
|
|
var materials = new LeadMaterialSnapshot[members.Count];
|
|
for (var i = 0; i < members.Count; i++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
try
|
|
{
|
|
materials[i] = LeadMaterialSnapshot.Capture(FixedLeadReview.Clean(members[i]), members[i].Location, token);
|
|
}
|
|
catch (Exception exception) when (IsInvalid(exception))
|
|
{
|
|
materials[i] = null;
|
|
}
|
|
if (materials[i] is not { IsComplete: true })
|
|
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, Number(plate, members[i]), null,
|
|
"Common-line lead check: " + (materials[i]?.Reason ?? "the clean material could not be captured.")));
|
|
}
|
|
for (var i = 0; i < members.Count; i++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (materials[i] is not { IsComplete: true } own)
|
|
continue;
|
|
var partners = Enumerable.Range(0, members.Count)
|
|
.Where(j => j != i && materials[j] is { IsComplete: true }).ToArray();
|
|
OwnedExecution execution;
|
|
try
|
|
{
|
|
execution = ExecutionMotionReader.Read(members[i].Program, members[i].Location, null, token);
|
|
}
|
|
catch (Exception exception) when (IsInvalid(exception))
|
|
{
|
|
continue; // The rapid check reports the unreadable program.
|
|
}
|
|
var failures = new List<FixedLeadFailure>();
|
|
var result = LeadPathValidator.Check(execution, own, partners.Select(j => materials[j]).ToArray(),
|
|
_ => true, failures, token);
|
|
if (!result.IsComplete)
|
|
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, Number(plate, members[i]), null,
|
|
"Common-line lead check: " + result.Reason));
|
|
// Only partners matter here: a lead into the part's own material is the lead checks' to report.
|
|
foreach (var failure in failures.Where(f => f.Obstacle is int))
|
|
{
|
|
var partner = members[partners[failure.Obstacle!.Value]];
|
|
var move = execution.Motions[failure.Motion];
|
|
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, members[i]),
|
|
Number(plate, partner), move.Layer == LayerType.CommonLine
|
|
? $"A shared cut of '{members[i].BaseDrawing.Name}' runs into '{partner.BaseDrawing.Name}', its "
|
|
+ "common-line partner. Rejoin the parts one kerf apart and plan cutting again."
|
|
: $"A lead of '{members[i].BaseDrawing.Name}' runs into '{partner.BaseDrawing.Name}', its "
|
|
+ "common-line partner one kerf away. Move the lead off the shared edge (Plan Cutting does).")
|
|
{ Location = move.Curve?.Midpoint ?? move.End });
|
|
}
|
|
}
|
|
}
|
|
|
|
private static int? Number(Plate plate, Part part)
|
|
{
|
|
var index = plate.Parts.IndexOf(part);
|
|
return index < 0 ? null : index + 1;
|
|
}
|
|
|
|
private static bool IsInvalid(Exception exception) => exception is
|
|
ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException;
|
|
}
|