mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 21:35:44 -05:00
feat(cutting): start the owner's outline on its common line
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>
This commit is contained in:
1 parent
bff11dbc3c
commit
74937b7952
15 files changed
+530
-337
No files matched your search
@@ -72,7 +72,7 @@ Read the relevant contract before changing its behavior:
|
||||
- [Breakoffs](docs/breakoffs.md): tabbed scrap; `Break`-layer open cuts that are cut but are not material
|
||||
- [Tabbed cutouts](docs/tabbed-cutouts.md): holes cut in pieces around `Tab`-layer spans that are material but never cut; nesting reads material through `NestingMaterial.ForNesting`, so nothing nests in them
|
||||
- [Dimensions](docs/dimensions.md): CAD Converter annotations that store placement only, are measured from the cut geometry when shown and never affect the program
|
||||
- [Common-line cutting](docs/common-line-cutting.md): parts one kerf apart in `Plate.CommonLines` share an edge; spans are derived, never stored, and spacing checks hold group members to the kerf. Plan Cutting cuts each shared span once (owner first, open `CommonLine` centerline cut, then each member's remainder) unless the plate is set to cut them twice; only posts implementing `ICommonLinePostProcessor` (CI Fiber) may write it, others refuse. Machine trial pending. Auto Nest and Fill Area pair `AllowCommonLine` drawings at the nest's `CommonLineUsage` (Off by default, byte-identical to no common lines); `NestLayoutCheck` exempts only explicit result groups
|
||||
- [Common-line cutting](docs/common-line-cutting.md): parts one kerf apart in `Plate.CommonLines` share an edge; spans are derived, never stored, and spacing checks hold group members to the kerf. Plan Cutting cuts each shared span once (the owner, cut first, starts its compensated outside on the common line; partners cut only their remainders) unless the plate is set to cut them twice; only posts implementing `ICommonLinePostProcessor` (CI Fiber) may write it, others refuse. Machine trial pending. Auto Nest and Fill Area pair `AllowCommonLine` drawings at the nest's `CommonLineUsage` (Off by default, byte-identical to no common lines); `NestLayoutCheck` exempts only explicit result groups
|
||||
- [Support tabs](docs/support-tabs.md): the nest's support grid, the center-of-gravity check, and Plan Cutting's tab placement for parts the grid cannot hold level
|
||||
- [Material-overlap diagnostics](docs/geometry/visual-overlap-check.md)
|
||||
- [Automatic cutoffs and sequencing](docs/automatic-scrap-cutoffs.md)
|
||||
|
||||
@@ -47,7 +47,8 @@ internal static class ContourProgramVerifier
|
||||
if (material == null || !material.IsComplete) return false;
|
||||
if (selected != null && !SameEmission(actual, selected.Expected, token, ref budget)) return false;
|
||||
var visited = new HashSet<int>();
|
||||
var piecesCut = new Dictionary<int, HashSet<int>>();
|
||||
// Per tabbed ring: what each run cut, as (span start, from, to) along its span.
|
||||
var piecesCut = new Dictionary<int, List<(int Span, double From, double To)>>();
|
||||
foreach (var run in Runs(actual).Where(r => !r.Rapid && r.Kind == LayerType.Cut))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -76,12 +77,13 @@ internal static class ContourProgramVerifier
|
||||
var tabs = material.Tabs[ringIndex];
|
||||
if (tabs.Any(tab => tab))
|
||||
{
|
||||
// A tabbed cutout: each run cuts exactly one span between tabs, whole and once.
|
||||
if (!CutsTabbedSpan(run.Curves, material.Rings[ringIndex], tabs, entityIndex, token, ref budget)
|
||||
|| !(piecesCut.TryGetValue(ringIndex, out var cut) ? cut : piecesCut[ringIndex] = new()).Add(entityIndex))
|
||||
// A tabbed ring: each run follows one span between tabs and never crosses a tab. A
|
||||
// span is usually one run; a common-line member that starts on its common line cuts
|
||||
// the span holding that start in two, each from where the other began or ended.
|
||||
if (CutTabbedSpan(run.Curves, material.Rings[ringIndex], tabs, entityIndex, token, ref budget)
|
||||
is not { } piece)
|
||||
return false;
|
||||
if (piecesCut[ringIndex].Count == SpanStarts(tabs).Count)
|
||||
visited.Add(ringIndex);
|
||||
(piecesCut.TryGetValue(ringIndex, out var cut) ? cut : piecesCut[ringIndex] = new()).Add(piece);
|
||||
continue;
|
||||
}
|
||||
if (!visited.Add(ringIndex)) return false;
|
||||
@@ -109,6 +111,27 @@ internal static class ContourProgramVerifier
|
||||
if (!Follows(run.Curves, ring, entityIndex, ring[entityIndex].DistanceAlong(first.Start),
|
||||
true, tolerance, token, ref budget)) return false;
|
||||
}
|
||||
// Every span of a tabbed ring is cut exactly once: its pieces tile it with no gap or overlap.
|
||||
foreach (var (ringIndex, pieces) in piecesCut)
|
||||
{
|
||||
var ring = material.Rings[ringIndex];
|
||||
var tabs = material.Tabs[ringIndex];
|
||||
var tolerance = AdmittedClosureSlack(ring);
|
||||
foreach (var start in SpanStarts(tabs))
|
||||
{
|
||||
var length = Span(ring, tabs, start).Sum(c => c.Length);
|
||||
var at = 0.0;
|
||||
foreach (var piece in pieces.Where(p => p.Span == start).OrderBy(p => p.From))
|
||||
{
|
||||
if (System.Math.Abs(piece.From - at) > tolerance)
|
||||
return false;
|
||||
at = piece.To;
|
||||
}
|
||||
if (System.Math.Abs(length - at) > tolerance)
|
||||
return false;
|
||||
}
|
||||
visited.Add(ringIndex);
|
||||
}
|
||||
return visited.Count == material.Rings.Count - (material.HasDisplayPerimeter ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -116,19 +139,41 @@ internal static class ContourProgramVerifier
|
||||
private static List<int> SpanStarts(bool[] tabs) => Enumerable.Range(0, tabs.Length)
|
||||
.Where(i => !tabs[i] && tabs[(i + tabs.Length - 1) % tabs.Length]).ToList();
|
||||
|
||||
// A run that starts exactly where a span between tabs starts and covers that span exactly,
|
||||
// stopping at the next tab.
|
||||
private static bool CutsTabbedSpan(IReadOnlyList<Curve> run, IReadOnlyList<Curve> ring, bool[] tabs,
|
||||
int entityIndex, CancellationToken token, ref int budget)
|
||||
// What a run along a tabbed ring cuts: its span (by the span's first curve) and where along the
|
||||
// span it starts and ends. Null unless the run follows the ring from its start without
|
||||
// reaching past the span's next tab.
|
||||
private static (int Span, double From, double To)? CutTabbedSpan(IReadOnlyList<Curve> run,
|
||||
IReadOnlyList<Curve> ring, bool[] tabs, int entityIndex, CancellationToken token, ref int budget)
|
||||
{
|
||||
if (tabs[entityIndex])
|
||||
return null;
|
||||
var tolerance = AdmittedClosureSlack(ring);
|
||||
if (!SpanStarts(tabs).Contains(entityIndex) || ring[entityIndex].DistanceAlong(run[0].Start) > tolerance)
|
||||
return false;
|
||||
var count = tabs.Length;
|
||||
var start = entityIndex;
|
||||
var from = ring[entityIndex].DistanceAlong(run[0].Start);
|
||||
for (var guard = 0; guard < count && !tabs[(start + count - 1) % count]; guard++)
|
||||
{
|
||||
start = (start + count - 1) % count;
|
||||
from += ring[start].Length;
|
||||
}
|
||||
var rest = new List<Curve>();
|
||||
for (var e = entityIndex; !tabs[e % count] && rest.Count < count; e++)
|
||||
rest.Add(ring[e % count]);
|
||||
var length = run.Sum(c => c.Length);
|
||||
var offset = ring[entityIndex].DistanceAlong(run[0].Start);
|
||||
if (length > rest.Sum(c => c.Length) - offset + tolerance
|
||||
|| !Follows(run, rest, 0, offset, false, tolerance, token, ref budget, partial: true))
|
||||
return null;
|
||||
return (start, from, from + length);
|
||||
}
|
||||
|
||||
// The curves of the span between tabs that starts at curve start.
|
||||
private static List<Curve> Span(IReadOnlyList<Curve> ring, bool[] tabs, int start)
|
||||
{
|
||||
var span = new List<Curve>();
|
||||
for (var e = entityIndex; !tabs[e % tabs.Length]; e++)
|
||||
for (var e = start; !tabs[e % tabs.Length] && span.Count < tabs.Length; e++)
|
||||
span.Add(ring[e % tabs.Length]);
|
||||
return System.Math.Abs(run.Sum(c => c.Length) - span.Sum(c => c.Length)) <= tolerance
|
||||
&& Follows(run, span, 0, 0, false, tolerance, token, ref budget);
|
||||
return span;
|
||||
}
|
||||
|
||||
// Zero for an exactly closed ring: a ring may lawfully close within the nominal closure
|
||||
@@ -163,8 +208,10 @@ internal static class ContourProgramVerifier
|
||||
// collinear moves and a cyclic ring's reindexing do not change coverage.
|
||||
// `tolerance` is the admitted positional slack for this pair of streams (the exact epsilon
|
||||
// unless the nominal ring closes only within the nominal closure tolerance).
|
||||
// `partial` accepts an actual stream that ends before the expected one does.
|
||||
private static bool Follows(IReadOnlyList<Curve> actual, IReadOnlyList<Curve> expected,
|
||||
int index, double offset, bool cyclic, double tolerance, CancellationToken token, ref int budget)
|
||||
int index, double offset, bool cyclic, double tolerance, CancellationToken token, ref int budget,
|
||||
bool partial = false)
|
||||
{
|
||||
foreach (var curve in actual)
|
||||
{
|
||||
@@ -201,7 +248,7 @@ internal static class ContourProgramVerifier
|
||||
}
|
||||
}
|
||||
}
|
||||
return cyclic || index == expected.Count || index == expected.Count - 1
|
||||
return cyclic || partial || index == expected.Count || index == expected.Count - 1
|
||||
&& expected[index].Length - offset <= tolerance;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,39 +25,20 @@ public sealed record ContourChoice(int ContourOrdinal, int EntityOrdinal, Vector
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The shared cuts a common-line member makes before its remainder (see
|
||||
/// docs/common-line-cutting.md): open centerline cuts in part coordinates, each from
|
||||
/// <see cref="CommonLineCutPath.Start"/> to <see cref="CommonLineCutPath.End"/>, with the group's kerf.
|
||||
/// How a common-line member's outside is cut once (see docs/common-line-cutting.md), with the
|
||||
/// group's kerf. The member cuts its outside compensated, so each shared edge it cuts lands on the
|
||||
/// kerf centerline: that edge is the common line. Spans an earlier member cut carry the Tab layer
|
||||
/// and are never cut again. A member that cuts a common line itself (<see cref="Entry"/> set)
|
||||
/// starts there: at <see cref="Entry"/> (part coordinates), the vertex where its shared edge
|
||||
/// begins, with a straight lead-in from <see cref="Pierce"/> along that edge's axis, and runs on
|
||||
/// round its perimeter with no lead-out, closing at the entry or stopping in an earlier member's
|
||||
/// kerf. Its other stretches, and every stretch of a member without an entry, are remainder pieces
|
||||
/// entered in the kerf before them.
|
||||
/// </summary>
|
||||
public sealed record CommonLinePerimeter(double Kerf, IReadOnlyList<CommonLineCutPath> SharedCuts);
|
||||
|
||||
/// <summary>
|
||||
/// One shared cut on a common line's centerline, in part coordinates and cut direction: straight,
|
||||
/// or through the corners of a Z or L shared line.
|
||||
/// </summary>
|
||||
public sealed record CommonLineCutPath(IReadOnlyList<Vector> Points)
|
||||
public sealed record CommonLinePerimeter(double Kerf, Vector? Entry = null, Vector? Pierce = null)
|
||||
{
|
||||
public CommonLineCutPath(Vector start, Vector end) : this(new[] { start, end })
|
||||
{
|
||||
}
|
||||
|
||||
public Vector Start => Points[0];
|
||||
|
||||
public Vector End => Points[^1];
|
||||
|
||||
/// <summary>The same path cut from its other end.</summary>
|
||||
public CommonLineCutPath Reversed() => new(Points.Reverse().ToArray());
|
||||
|
||||
/// <summary>Whether the path has two or more points and no degenerate leg.</summary>
|
||||
internal bool IsNondegenerate(double epsilon)
|
||||
{
|
||||
if (Points is not { Count: >= 2 })
|
||||
return false;
|
||||
for (var i = 1; i < Points.Count; i++)
|
||||
if (Points[i - 1].DistanceTo(Points[i]) <= epsilon)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
/// <summary>Whether the member starts its outside on a common line it cuts.</summary>
|
||||
public bool StartsOnCommonLine => Entry != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,8 +72,8 @@ public sealed class PreparedContours
|
||||
// never-executed DisplayPerimeterTail. Null for an ordinary cut perimeter.
|
||||
private readonly Shape displayOutline;
|
||||
private bool hasCutPerimeter => displayOutline == null;
|
||||
// Set for a common-line member: the perimeter's shared spans are Tab entities, never cut by
|
||||
// this part. It is cut as its shared cuts, then its remainder pieces, from one fixed entry.
|
||||
// Set for a common-line member, cut from one fixed entry: its whole perimeter from where its
|
||||
// first shared edge begins, or its remainder pieces between the Tab spans earlier members cut.
|
||||
private readonly CommonLinePerimeter commonLine;
|
||||
private readonly ContourChoice commonLineEntry;
|
||||
|
||||
@@ -116,7 +97,19 @@ public sealed class PreparedContours
|
||||
this.parameters = parameters;
|
||||
pinned = new ContourChoice[shapes.Length];
|
||||
pinNumbers = new int[shapes.Length];
|
||||
if (commonLine != null)
|
||||
if (commonLine is { Entry: { } entry })
|
||||
{
|
||||
// The outside starts where a shared edge it cuts begins, pierced off it.
|
||||
var perimeter = shapes[^1];
|
||||
var entity = perimeter.Entities.FindIndex(e => e.Layer != SpecialLayers.Tab
|
||||
&& Start(e).DistanceTo(entry) <= PostVerificationGeometry.Epsilon);
|
||||
commonLineEntry = new ContourChoice(shapes.Length - 1, entity, Start(perimeter.Entities[entity]))
|
||||
{
|
||||
Owner = this,
|
||||
Pierce = commonLine.Pierce,
|
||||
};
|
||||
}
|
||||
else if (commonLine != null)
|
||||
{
|
||||
// The remainder's first piece starts where a shared span ends: the one fixed entry.
|
||||
var perimeter = shapes[^1];
|
||||
@@ -225,11 +218,11 @@ public sealed class PreparedContours
|
||||
|
||||
/// <summary>
|
||||
/// True for a common-line member (see <see cref="CommonLinePerimeter"/>): its perimeter has one
|
||||
/// fixed entry and is emitted as its shared cuts, then its remainder pieces.
|
||||
/// fixed entry and is emitted whole from its common line, or as its remainder pieces.
|
||||
/// </summary>
|
||||
public bool IsCommonLinePerimeter => commonLine != null;
|
||||
|
||||
/// <summary>The member's shared cuts and kerf, or null for any other part.</summary>
|
||||
/// <summary>How the member's outside is cut once, or null for any other part.</summary>
|
||||
public CommonLinePerimeter CommonLine => commonLine;
|
||||
|
||||
public int Count => shapes.Length;
|
||||
@@ -296,9 +289,10 @@ public sealed class PreparedContours
|
||||
|
||||
private CuttingParameters ParametersFor(int contourOrdinal) => contourParameters?[contourOrdinal] ?? parameters;
|
||||
|
||||
/// <param name="commonLine">For a common-line member: its shared cuts. The program's perimeter
|
||||
/// must carry the member's shared spans on the Tab layer (CommonLineCutting.MarkSharedSpans); it
|
||||
/// is then the perimeter, cut in pieces between them, rather than a tabbed cutout.</param>
|
||||
/// <param name="commonLine">For a common-line member: how its outside is cut once. With an
|
||||
/// entry, the program's perimeter is closed and starts there. Without one, it must carry the
|
||||
/// spans earlier members cut on the Tab layer (CommonLineCutting.MarkSharedSpans); it is then
|
||||
/// the perimeter, cut in pieces between them, rather than a tabbed cutout.</param>
|
||||
public static PreparedContours Capture(Program program, CuttingParameters parameters, CancellationToken token = default,
|
||||
CommonLinePerimeter commonLine = null)
|
||||
{
|
||||
@@ -367,12 +361,32 @@ public sealed class PreparedContours
|
||||
{
|
||||
if (displayPerimeter != null || current.Entities.Count != 0)
|
||||
throw new ArgumentException("A common-line member needs one closed cut perimeter.");
|
||||
if (!double.IsFinite(commonLine.Kerf) || commonLine.Kerf <= 0 || commonLine.SharedCuts == null
|
||||
|| commonLine.SharedCuts.Any(c => c == null || !c.IsNondegenerate(PostVerificationGeometry.Epsilon)))
|
||||
throw new ArgumentException("A common-line member needs a positive kerf and nondegenerate shared cuts.");
|
||||
// The outside carries the shared spans as tabs; every other contour lies inside it.
|
||||
if (!double.IsFinite(commonLine.Kerf) || commonLine.Kerf <= 0
|
||||
|| (commonLine.Entry == null) != (commonLine.Pierce == null))
|
||||
throw new ArgumentException("A common-line member needs a positive kerf, and an entry with its pierce.");
|
||||
var outside = tabbedCutouts.Concat(contours).MaxBy(shape => shape.BoundingBox.Width * shape.BoundingBox.Length);
|
||||
if (outside == null || !tabbedCutouts.Contains(outside)
|
||||
if (commonLine is { Entry: { } entry, Pierce: { } pierce })
|
||||
{
|
||||
// Cut from its entry vertex on a common line; every other contour lies inside it.
|
||||
PostVerificationGeometry.Validate(entry);
|
||||
PostVerificationGeometry.Validate(pierce);
|
||||
if (outside == null
|
||||
|| !outside.Entities.Exists(e => e.Layer != SpecialLayers.Tab
|
||||
&& Start(e).DistanceTo(entry) <= PostVerificationGeometry.Epsilon)
|
||||
|| pierce.DistanceTo(entry) <= PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("A common-line member's perimeter does not start at its common line.");
|
||||
if (contours.Contains(outside))
|
||||
{
|
||||
contours.Remove(outside);
|
||||
contours.Add(outside);
|
||||
foreach (var cut in breakCuts)
|
||||
cut.UpdateBounds();
|
||||
return new(contours.ToArray(), scribes, breakCuts, tabbedCutouts, ownedParameters, null, null,
|
||||
commonLine);
|
||||
}
|
||||
}
|
||||
// The outside carries the shared spans earlier members cut as tabs; every other contour lies inside it.
|
||||
else if (outside == null || !tabbedCutouts.Contains(outside)
|
||||
|| ContourCuttingStrategy.TabbedCutoutPieces(outside).Count == 0)
|
||||
throw new ArgumentException("A common-line member's perimeter does not carry its shared spans.");
|
||||
// Each remainder piece is pierced in the kerf of the span before it, the external
|
||||
@@ -395,7 +409,7 @@ public sealed class PreparedContours
|
||||
foreach (var cut in breakCuts)
|
||||
cut.UpdateBounds();
|
||||
return new(contours.ToArray(), scribes, breakCuts, tabbedCutouts, ownedParameters, null, null,
|
||||
commonLine with { SharedCuts = commonLine.SharedCuts.ToArray() });
|
||||
commonLine);
|
||||
}
|
||||
if (displayPerimeter != null)
|
||||
{
|
||||
@@ -1068,10 +1082,10 @@ public sealed class PreparedContours
|
||||
// A pinned contour is cut exactly as pinned: no other entry may be emitted for it.
|
||||
if (pinned[choice.ContourOrdinal] is { } pin && pin != choice)
|
||||
throw new ArgumentException("A pinned contour keeps its drawing pin's entry.");
|
||||
// A common-line perimeter is cut from its one fixed entry, after its shared cuts.
|
||||
// A common-line perimeter is cut from its one fixed entry.
|
||||
if (commonLineEntry != null && choice.ContourOrdinal == PerimeterOrdinal
|
||||
&& (choice.EntityOrdinal != commonLineEntry.EntityOrdinal || choice.Point != commonLineEntry.Point
|
||||
|| choice.Pierce != null))
|
||||
|| choice.Pierce != commonLineEntry.Pierce))
|
||||
throw new ArgumentException("A common-line member's perimeter is cut from its fixed entry.");
|
||||
if (choice.Pierce is { } pierce)
|
||||
{
|
||||
|
||||
@@ -318,6 +318,26 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
var inSlug = false;
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
if (commonLine is { StartsOnCommonLine: true } && choice.ContourOrdinal == perimeterOrdinal
|
||||
&& !IsTabbedCutout(shapes[perimeterOrdinal]))
|
||||
{
|
||||
// Cut whole from its common line: the straight lead-in from the choice's
|
||||
// pierce runs along the shared edge, and the perimeter ends where it began,
|
||||
// beside its partner, so it has no lead-out and no tab.
|
||||
var whole = CuttingPlanning.OwnedCuttingParameters.Copy(shared);
|
||||
whole.ExternalLeadOut = new NoLeadOut();
|
||||
whole.TabsEnabled = false;
|
||||
Parameters = whole;
|
||||
var start = result.Codes.Count;
|
||||
EmitContour(result, shapes[perimeterOrdinal], choice.Point,
|
||||
shapes[perimeterOrdinal].Entities[choice.EntityOrdinal], ContourType.External,
|
||||
exactCirclePrograms: true, scrapPierce: choice.Pierce, stopAtCenterLine: false,
|
||||
memoContour: choice.ContourOrdinal);
|
||||
if (inSlug && start < result.Codes.Count && result.Codes[start] is RapidMove)
|
||||
result.Codes.Insert(start, new Comment(HeadRaise.Marker));
|
||||
inSlug = false;
|
||||
continue;
|
||||
}
|
||||
if (commonLine != null && choice.ContourOrdinal == perimeterOrdinal)
|
||||
{
|
||||
// The rapid out of a cutout's slug before it is still made head up.
|
||||
@@ -1581,25 +1601,45 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A common-line member's outside, cut once per shared edge (docs/common-line-cutting.md):
|
||||
/// first each shared cut the member owns, an open centerline cut entered and left along
|
||||
/// its own axis with straight leads as long as the external lead-in's and lead-out's
|
||||
/// reach; then each remainder piece (the perimeter between its shared spans, which lie
|
||||
/// on the Tab layer) in the perimeter's own direction. A piece starts where a shared span
|
||||
/// ends. It is pierced in that span's opened kerf on the centerline, the external lead-in's
|
||||
/// reach back along the span, and enters along a straight lead-in that ends at the piece's
|
||||
/// start, so compensation starts in the kerf. It has no lead-out: it ends in the kerf
|
||||
/// at the next shared span. Capture checks that every lead fits; emission never shortens one.
|
||||
/// A common-line member's outside around the shared spans earlier members cut (on the Tab
|
||||
/// layer; docs/common-line-cutting.md), each piece in the perimeter's own direction. With a
|
||||
/// common-line entry, the first piece starts there, pierced in scrap with a straight lead-in
|
||||
/// along the shared edge, and runs to the next span; the stretch before the entry is cut
|
||||
/// last and ends at it. Every other piece starts where a shared span ends. It is pierced in that span's opened
|
||||
/// kerf on the centerline, the external lead-in's reach back along the span, and enters along
|
||||
/// a straight lead-in that ends at the piece's start, so compensation starts in the kerf. It
|
||||
/// has no lead-out: it ends in the kerf at the next shared span. Capture checks that every
|
||||
/// lead fits; emission never shortens one.
|
||||
/// </summary>
|
||||
private void EmitCommonLinePerimeter(Program program, Shape perimeter, CuttingPlanning.CommonLinePerimeter commonLine)
|
||||
{
|
||||
var winding = DetermineWinding(perimeter);
|
||||
var leadIn = Parameters?.ExternalLeadIn ?? new NoLeadIn();
|
||||
var leadOut = Parameters?.ExternalLeadOut ?? new NoLeadOut();
|
||||
foreach (var cut in commonLine.SharedCuts)
|
||||
program.Codes.AddRange(CommonLineSharedCutCodes(cut, leadIn, leadOut));
|
||||
var entities = perimeter.Entities;
|
||||
foreach (var piece in TabbedCutoutPieces(perimeter))
|
||||
var pieces = TabbedCutoutPieces(perimeter);
|
||||
if (commonLine.Entry is { } start && commonLine.Pierce is { } pierce)
|
||||
{
|
||||
// The piece holding the entry is cut from it to its end first, then the pieces after
|
||||
// it in travel order, and its stretch before the entry last.
|
||||
var at = pieces.FindIndex(p => p.Entities.Exists(e => EntityStartPoint(e).DistanceTo(start) <= Tolerance.Epsilon));
|
||||
var piece = pieces[at];
|
||||
var split = piece.Entities.FindIndex(e => EntityStartPoint(e).DistanceTo(start) <= Tolerance.Epsilon);
|
||||
var head = new Shape();
|
||||
head.Entities.AddRange(piece.Entities.Skip(split));
|
||||
program.Codes.Add(new RapidMove(pierce));
|
||||
program.Codes.Add(new Comment(CommonLineCuts.RemainderMarker(winding)));
|
||||
program.Codes.Add(new LinearMove(start) { Layer = LayerType.Leadin });
|
||||
program.Codes.AddRange(ConvertShapeToMoves(head, start));
|
||||
var rest = pieces.Skip(at + 1).Concat(pieces.Take(at)).ToList();
|
||||
if (split > 0)
|
||||
{
|
||||
var tail = new Shape();
|
||||
tail.Entities.AddRange(piece.Entities.Take(split));
|
||||
rest.Add(tail);
|
||||
}
|
||||
pieces = rest;
|
||||
}
|
||||
foreach (var piece in pieces)
|
||||
{
|
||||
var first = piece.Entities[0];
|
||||
var index = entities.IndexOf(first);
|
||||
@@ -1628,45 +1668,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
return span.EndPoint - along * reach + outward * (kerf / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One shared cut, absolute in its part's frame: a rapid to the pierce on the first leg's
|
||||
/// axis before its start, the straight lead-in, the open centerline cut through every
|
||||
/// corner of a Z or L path, and the straight lead-out on past its end along the last leg,
|
||||
/// each lead as long as the configured one reaches.
|
||||
/// </summary>
|
||||
internal static List<ICode> CommonLineSharedCutCodes(CuttingPlanning.CommonLineCutPath cut, LeadIn leadIn,
|
||||
LeadOut leadOut)
|
||||
{
|
||||
var codes = new List<ICode>();
|
||||
var points = cut.Points;
|
||||
var entry = (points[1] - points[0]).Normalize();
|
||||
var reach = CommonLineLeadInReach(leadIn);
|
||||
codes.Add(new RapidMove(cut.Start - entry * reach));
|
||||
if (reach > Tolerance.Epsilon)
|
||||
codes.Add(new LinearMove(cut.Start) { Layer = LayerType.Leadin });
|
||||
for (var i = 1; i < points.Count; i++)
|
||||
codes.Add(new LinearMove(points[i]) { Layer = LayerType.CommonLine });
|
||||
var leave = (points[^1] - points[^2]).Normalize();
|
||||
var exit = CommonLineLeadOutReach(leadOut);
|
||||
if (exit > Tolerance.Epsilon)
|
||||
codes.Add(new LinearMove(cut.End + leave * exit) { Layer = LayerType.Leadout });
|
||||
return codes;
|
||||
}
|
||||
|
||||
/// <summary>How far a common-line lead-in reaches: the external lead-in's pierce distance.</summary>
|
||||
internal static double CommonLineLeadInReach(LeadIn leadIn) =>
|
||||
leadIn.GetPiercePoint(Vector.Zero, 0, RotationType.CW).DistanceTo(Vector.Zero);
|
||||
|
||||
/// <summary>How far a common-line shared cut's lead-out reaches: the external lead-out's.</summary>
|
||||
internal static double CommonLineLeadOutReach(LeadOut leadOut)
|
||||
{
|
||||
var reach = 0.0;
|
||||
foreach (var code in leadOut.Generate(Vector.Zero, 0, RotationType.CW))
|
||||
if (code is Motion motion)
|
||||
reach = System.Math.Max(reach, motion.EndPoint.DistanceTo(Vector.Zero));
|
||||
return reach;
|
||||
}
|
||||
|
||||
/// <summary>Whether <paramref name="shape"/> has <see cref="SpecialLayers.Tab"/> spans: a tabbed cutout.</summary>
|
||||
internal static bool IsTabbedCutout(Shape shape) => shape.Entities.Exists(e => e.Layer == SpecialLayers.Tab);
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ using OpenNest.Geometry;
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// One shared cut of a common-line group, cut once on the centerline by <see cref="Owner"/>, the
|
||||
/// member of the pair cut first: a straight span, or spans chained at corners (a Z or L shared
|
||||
/// line) cut as one open path. <see cref="Partner"/> is cut later; its remainder ends in the
|
||||
/// kerf this cut opened.
|
||||
/// One shared cut of a common-line group: a straight span, or spans chained at corners (a Z or
|
||||
/// L shared line). <see cref="Owner"/>, the member of the pair cut first, cuts it as part of its
|
||||
/// own compensated perimeter, which lands on the kerf centerline. <see cref="Partner"/> is cut
|
||||
/// later; its remainder ends in the kerf this cut opened.
|
||||
/// </summary>
|
||||
public sealed record CommonLineSharedCut(CommonLinePath Path, Part Owner, Part Partner);
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace OpenNest
|
||||
public IEnumerable<CommonLineSharedCut> CutsOwnedBy(Part member) =>
|
||||
Cuts.Where(cut => ReferenceEquals(cut.Owner, member));
|
||||
|
||||
/// <summary>
|
||||
/// The shared paths an earlier member cuts and <paramref name="member"/> therefore never
|
||||
/// does: its remainder ends in their kerf.
|
||||
/// </summary>
|
||||
public IEnumerable<CommonLinePath> PathsCutBefore(Part member) => Cuts
|
||||
.Where(cut => ReferenceEquals(cut.Partner, member))
|
||||
.Select(cut => cut.Path);
|
||||
|
||||
/// <summary>Every shared path <paramref name="member"/> shares, whoever cuts it.</summary>
|
||||
public IEnumerable<CommonLinePath> PathsOf(Part member) => Cuts
|
||||
.Where(cut => ReferenceEquals(cut.Owner, member) || ReferenceEquals(cut.Partner, member))
|
||||
@@ -157,6 +165,67 @@ namespace OpenNest
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where a member that cuts <paramref name="paths"/> as part of its own perimeter can start,
|
||||
/// so that each common line leads straight into the rest of the perimeter: for each path,
|
||||
/// the start of the perimeter edge (a top-level straight move of <paramref name="clean"/>)
|
||||
/// along which the member's own travel enters the path, a pierce <paramref name="reach"/>
|
||||
/// back along that edge's axis, and the edge's end. Part coordinates (the program placed at
|
||||
/// <paramref name="location"/>), in path order.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<(Vector Entry, Vector Pierce, Vector Next)> PerimeterEntries(Program clean,
|
||||
Vector location, IReadOnlyList<CommonLinePath> paths, double kerf, double reach)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(clean);
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
if (!double.IsFinite(kerf) || kerf <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(kerf));
|
||||
var program = (Program)clean.Clone();
|
||||
program.Mode = Mode.Absolute;
|
||||
var edges = new List<(Vector Start, Vector End)>();
|
||||
Vector? position = null;
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
if (code is LinearMove line && position is { } from && line.Layer is LayerType.Cut or LayerType.Display)
|
||||
edges.Add((from, line.EndPoint));
|
||||
position = code switch
|
||||
{
|
||||
Motion motion => motion.EndPoint,
|
||||
SubProgramCall => null,
|
||||
_ => position,
|
||||
};
|
||||
}
|
||||
var entries = new List<(Vector Entry, Vector Pierce, Vector Next)>();
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var points = path.Points.Select(point => point - location).ToArray();
|
||||
// Each end of the path, with the direction into the path from there.
|
||||
foreach (var (end, inward) in new[]
|
||||
{
|
||||
(points[0], (points[1] - points[0]).Normalize()),
|
||||
(points[^1], (points[^2] - points[^1]).Normalize()),
|
||||
})
|
||||
foreach (var (start, stop) in edges)
|
||||
{
|
||||
var length = start.DistanceTo(stop);
|
||||
if (length <= Math.Tolerance.Epsilon)
|
||||
continue;
|
||||
var along = new Vector((stop.X - start.X) / length, (stop.Y - start.Y) / length);
|
||||
if (System.Math.Abs(Cross(along, inward)) > 1e-6 || Dot(along, inward) <= 0)
|
||||
continue;
|
||||
if (System.Math.Abs(System.Math.Abs(Cross(along, end - start)) - kerf / 2) > CommonLineGeometry.GapTolerance)
|
||||
continue;
|
||||
var t = Dot(end - start, along);
|
||||
if (t < -CommonLineGeometry.GapTolerance || t > length + CommonLineGeometry.GapTolerance)
|
||||
continue;
|
||||
var entry = (start, start - along * reach, stop);
|
||||
if (!entries.Contains(entry))
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// One straight leg of a shared path in part coordinates. An end at a corner snaps to the
|
||||
// edge's own vertex within kerf/2 · tan(turn/2) of the centerline corner's projection.
|
||||
private readonly record struct Leg(Vector Start, Vector End, double StartSnap, double EndSnap);
|
||||
|
||||
@@ -12,8 +12,9 @@ namespace OpenNest.Diagnostics;
|
||||
/// 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,
|
||||
/// on its centerline, by the member cut first; one set to cut them twice must have no shared cuts.
|
||||
/// 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
|
||||
@@ -48,13 +49,17 @@ internal static class CommonLineReview
|
||||
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.Layer == LayerType.CommonLine && motion.Curve != null)
|
||||
.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))
|
||||
@@ -64,32 +69,36 @@ internal static class CommonLineReview
|
||||
}
|
||||
if (plate.CutCommonLinesTwice)
|
||||
{
|
||||
if (cuts.Count > 0)
|
||||
findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, cuts[0].Part), null,
|
||||
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 = cuts[0].Motion.Curve.Midpoint });
|
||||
{ Location = singlePass.BoundingBox?.Center });
|
||||
return;
|
||||
}
|
||||
foreach (var span in check.Spans)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var covering = cuts.Where(cut => Covers(cut.Motion, span)).ToList();
|
||||
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 once on its centerline. Plan cutting cuts each shared line once; "
|
||||
"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. Plan cutting again before posting.")
|
||||
"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;
|
||||
@@ -101,18 +110,23 @@ internal static class CommonLineReview
|
||||
}
|
||||
}
|
||||
|
||||
// A shared cut on the span's centerline that covers the whole span.
|
||||
private static bool Covers(ExecutionMotion motion, CommonLineSpan span)
|
||||
// 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) => System.Math.Abs((point.X - span.Start.X) * along.Y - (point.Y - span.Start.Y) * along.X);
|
||||
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;
|
||||
if (Offset(curve.Start) > tolerance || Offset(curve.End) > tolerance)
|
||||
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));
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Capture of a plate's common-line groups cut once (docs/common-line-cutting.md). Each group is
|
||||
/// cut member by member in <see cref="CommonLineCutting.Order"/>'s order: a member's holes, the
|
||||
/// shared cuts it owns (open centerline cuts), then its remainder pieces, which end in kerf those
|
||||
/// shared cuts, or an earlier member's, opened. The owner of a shared cut therefore precedes its
|
||||
/// partner. Shared spans are derived from the placed geometry at capture; a broken group refuses.
|
||||
/// cut member by member in <see cref="CommonLineCutting.Order"/>'s order: a member's holes, then
|
||||
/// its outside. A member no earlier member shares with cuts its whole perimeter from one of its
|
||||
/// common lines; any other cuts its remainder pieces, which end in kerf an earlier member opened.
|
||||
/// The owner of a shared cut therefore precedes its partner. Shared spans are derived from the placed geometry at capture; a broken group refuses.
|
||||
/// </summary>
|
||||
internal sealed class CommonLineCapture
|
||||
{
|
||||
@@ -48,7 +48,7 @@ internal sealed class CommonLineCapture
|
||||
var part = order.Members[i];
|
||||
if (IndexOf(request.Parts, part) < 0)
|
||||
throw new ArgumentException("A common-line member is not among the planned parts.");
|
||||
capture.members[part] = new Member(order, part, order.PathsOf(part).ToArray(),
|
||||
capture.members[part] = new Member(order, part, order.PathsCutBefore(part).ToArray(),
|
||||
order.CutsOwnedBy(part).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ internal sealed class CommonLineCapture
|
||||
|
||||
internal bool IsMember(Part part) => members.ContainsKey(part);
|
||||
|
||||
/// <summary>The member's owned clean program with its shared spans on the Tab layer.</summary>
|
||||
/// <summary>The member's owned clean program with the spans earlier members cut on the Tab layer.</summary>
|
||||
/// <exception cref="NotSupportedException">A span does not lie along the program's edges.</exception>
|
||||
internal Program Mark(Part part, Program ownedClean, Vector location, int ordinal)
|
||||
{
|
||||
@@ -84,48 +84,47 @@ internal sealed class CommonLineCapture
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The member's shared cuts in part coordinates, each cut from whichever end its straight
|
||||
/// leads fit: the cut, its lead-in and its lead-out may touch no material, and the pierce keeps
|
||||
/// the pierce zone. Leads are never shortened: a cut whose leads fit at neither end refuses,
|
||||
/// naming the span.
|
||||
/// How the member's outside is cut once. A member that cuts a common line itself starts its
|
||||
/// outside where its travel enters one, with a straight lead-in along that edge from the scrap:
|
||||
/// the first whose lead-in touches no material and keeps the pierce zone. Leads are never
|
||||
/// shortened: when none fits, planning refuses, naming the common line. A member that cuts none
|
||||
/// cuts its remainder from the kerf earlier members opened.
|
||||
/// </summary>
|
||||
/// <exception cref="NotSupportedException">No lead fits a shared cut.</exception>
|
||||
internal CommonLinePerimeter Perimeter(Part part, int ordinal, IReadOnlyList<Part> parts,
|
||||
/// <exception cref="NotSupportedException">No lead fits the start of any of its common lines.</exception>
|
||||
internal CommonLinePerimeter Perimeter(Part part, int ordinal, IReadOnlyList<Part> parts, Program clean,
|
||||
LeadMaterialSnapshot own, IReadOnlyList<LeadMaterialSnapshot> materials, CuttingParameters parameters,
|
||||
double pierceRadius, CancellationToken token)
|
||||
{
|
||||
var member = members[part];
|
||||
var kerf = member.Order.Group.Kerf;
|
||||
if (member.Owned.Count == 0)
|
||||
return new CommonLinePerimeter(kerf);
|
||||
var reach = ContourCuttingStrategy.CommonLineLeadInReach(parameters.ExternalLeadIn ?? new NoLeadIn());
|
||||
if (reach <= Math.Tolerance.Epsilon)
|
||||
throw new NotSupportedException($"Part {ordinal + 1} starts its perimeter on its common line, which "
|
||||
+ "needs an external lead-in with length to reach it from the scrap. Choose an external lead-in "
|
||||
+ "with length, or cut the plate's common lines twice.");
|
||||
var location = part.Location;
|
||||
var leadIn = parameters.ExternalLeadIn ?? new NoLeadIn();
|
||||
var leadOut = parameters.ExternalLeadOut ?? new NoLeadOut();
|
||||
var cuts = new List<CommonLineCutPath>();
|
||||
foreach (var cut in member.Owned)
|
||||
var owned = member.Owned.Select(cut => cut.Path).ToArray();
|
||||
string reason = null;
|
||||
foreach (var (entry, pierce, next) in CommonLineCutting.PerimeterEntries(clean, location, owned, kerf, reach))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var forward = new CommonLineCutPath(cut.Path.Points.Select(point => point - location).ToArray());
|
||||
var reverse = forward.Reversed();
|
||||
string reason = null;
|
||||
CommonLineCutPath chosen = null;
|
||||
foreach (var path in new[] { forward, reverse })
|
||||
{
|
||||
var program = new Program(Mode.Absolute);
|
||||
program.Codes.AddRange(ContourCuttingStrategy.CommonLineSharedCutCodes(path, leadIn, leadOut));
|
||||
var execution = ExecutionMotionReader.ReadSupported(program, location, null, token);
|
||||
var result = LeadPathValidator.Check(execution, own, materials, pierceRadius, token);
|
||||
if (result.IsComplete && result.IsClear)
|
||||
{
|
||||
chosen = path;
|
||||
break;
|
||||
}
|
||||
reason ??= result.Reason;
|
||||
}
|
||||
if (chosen == null)
|
||||
throw new NotSupportedException(FormattableString.Invariant(
|
||||
$"No lead fits the common line between parts {ordinal + 1} and {IndexOf(parts, cut.Partner) + 1} from ({cut.Path.Start.X:0.####}, {cut.Path.Start.Y:0.####}) to ({cut.Path.End.X:0.####}, {cut.Path.End.Y:0.####}) at either end: ")
|
||||
+ reason + " Shorten the external lead-in or lead-out, or cut the plate's common lines twice.");
|
||||
cuts.Add(chosen);
|
||||
var program = new Program(Mode.Absolute);
|
||||
program.Codes.Add(new RapidMove(pierce));
|
||||
program.Codes.Add(new LinearMove(entry) { Layer = LayerType.Leadin });
|
||||
program.Codes.Add(new LinearMove(next) { Layer = LayerType.Display });
|
||||
var execution = ExecutionMotionReader.ReadSupported(program, location, null, token);
|
||||
var result = LeadPathValidator.Check(execution, own, materials, pierceRadius, token);
|
||||
if (result.IsComplete && result.IsClear)
|
||||
return new CommonLinePerimeter(kerf, entry, pierce);
|
||||
reason ??= result.Reason;
|
||||
}
|
||||
return new CommonLinePerimeter(member.Order.Group.Kerf, cuts);
|
||||
var first = member.Owned[0];
|
||||
throw new NotSupportedException(FormattableString.Invariant(
|
||||
$"No lead fits the start of the common line between parts {ordinal + 1} and {IndexOf(parts, first.Partner) + 1} from ({first.Path.Start.X:0.####}, {first.Path.Start.Y:0.####}) to ({first.Path.End.X:0.####}, {first.Path.End.Y:0.####}): ")
|
||||
+ (reason ?? "it does not run along a straight edge of the part's program.")
|
||||
+ " Shorten the external lead-in, or cut the plate's common lines twice.");
|
||||
}
|
||||
|
||||
private static int IndexOf(IReadOnlyList<Part> parts, Part part)
|
||||
|
||||
@@ -139,7 +139,7 @@ public static class CuttingPlanService
|
||||
IncrementalClosureDrift.Close(ownedClean);
|
||||
// A baked-in seam overcut encloses no area; left in, the ring touches itself.
|
||||
SeamOvercut.Trim(ownedClean);
|
||||
// A common-line member never cuts its shared spans: they are its material's tabs.
|
||||
// A common-line member never cuts the spans earlier members cut: they are its material's tabs.
|
||||
var member = request.ConfirmedParameters != null && commonLines?.IsMember(source) == true;
|
||||
if (member)
|
||||
{
|
||||
@@ -238,7 +238,7 @@ public static class CuttingPlanService
|
||||
}
|
||||
|
||||
// A common-line member regenerates from its marked clean program: holes as usual, then its
|
||||
// shared cuts and remainder from one fixed entry. Its leads are checked here, so a lead that
|
||||
// whole perimeter from its common line, or its remainder, from one fixed entry. Its leads are checked here, so a lead that
|
||||
// fits nowhere refuses with the span named rather than as a search that ran out.
|
||||
private static FixedProgramPlacement CaptureMember(FixedProgramPlacement placed, Program clean,
|
||||
CommonLineCapture commonLines, CuttingPlanRequest request, IReadOnlyList<LeadMaterialSnapshot> materials,
|
||||
@@ -252,8 +252,8 @@ public static class CuttingPlanService
|
||||
PreparedContours prepared;
|
||||
try
|
||||
{
|
||||
var perimeter = commonLines.Perimeter(source, index, request.Parts, material, materials, parameters,
|
||||
pierceRadius, token);
|
||||
var perimeter = commonLines.Perimeter(source, index, request.Parts, clean, material, materials,
|
||||
parameters, pierceRadius, token);
|
||||
prepared = DrawingPinCapture.Prepare(source, index, clean, parameters, warnings, token, perimeter);
|
||||
if (!bestEffort)
|
||||
{
|
||||
@@ -262,8 +262,9 @@ public static class CuttingPlanService
|
||||
source.Location, null, token);
|
||||
var lead = LeadPathValidator.Check(probe, material, materials, pierceRadius, token);
|
||||
if (!lead.IsComplete || !lead.IsClear)
|
||||
throw new NotSupportedException("no lead-in fits its common-line remainder in the shared kerf: "
|
||||
+ lead.Reason);
|
||||
throw new NotSupportedException((perimeter.StartsOnCommonLine
|
||||
? "no lead-in fits the start of its common line: "
|
||||
: "no lead-in fits its common-line remainder in the shared kerf: ") + lead.Reason);
|
||||
}
|
||||
}
|
||||
catch (NotSupportedException exception) when (!exception.Message.StartsWith("Part ", StringComparison.Ordinal)
|
||||
|
||||
@@ -10,8 +10,10 @@ namespace OpenNest.Tests.CommonLines;
|
||||
|
||||
/// <summary>
|
||||
/// Plan Cutting cuts each common line once (docs/common-line-cutting.md): a group is cut member by
|
||||
/// member, each cutting its holes, then the shared cuts it owns on the centerline, then its
|
||||
/// remainder, which ends in the kerf the shared cut opened. The parts are PT42-like.
|
||||
/// member, each cutting its holes, then its outside compensated. A member that cuts a common line
|
||||
/// starts its outside there, entered along the shared edge from the scrap, so the line leads
|
||||
/// straight into its perimeter; its partner cuts only its remainder, which starts and ends in the
|
||||
/// kerf the common line opened. The parts are PT42-like.
|
||||
/// </summary>
|
||||
public class CommonLinePlanningTests
|
||||
{
|
||||
@@ -87,7 +89,7 @@ public class CommonLinePlanningTests
|
||||
private static bool IsCut(ExecutionMotion m) => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display;
|
||||
|
||||
[Fact]
|
||||
public void Pair_CutsTheSharedLineOnceOnTheCenterline()
|
||||
public void Pair_TheFirstPartCutsTheCommonLineAsTheStartOfItsPerimeter()
|
||||
{
|
||||
var plate = Row(2);
|
||||
|
||||
@@ -95,16 +97,27 @@ public class CommonLinePlanningTests
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
var cut = Assert.Single(result.ProposedOrder.SelectMany(Motions), m => m.Layer == LayerType.CommonLine);
|
||||
Assert.Equal(Centerline(0), cut.Curve.Start.X, 9);
|
||||
Assert.Equal(Centerline(0), cut.End.X, 9);
|
||||
Assert.Equal(Height - 2 * Chamfer, cut.Length, 9);
|
||||
// The member that cuts it comes first; its partner's remainder ends in its kerf.
|
||||
Assert.Same(plate.Parts[0], result.ProposedOrder[0].SourcePart);
|
||||
var all = result.ProposedOrder.SelectMany(Motions).ToArray();
|
||||
Assert.DoesNotContain(all, m => m.Layer == LayerType.CommonLine);
|
||||
// One pierce below the notch, a lead-in straight up the shared edge's axis, then the shared
|
||||
// edge itself and on round the perimeter to close where it began: no lead-out.
|
||||
var motions = Motions(result.ProposedOrder[0]).ToList();
|
||||
Assert.Single(motions, m => m.Layer == LayerType.Leadin);
|
||||
var lead = motions.Single(m => m.Layer == LayerType.Leadin);
|
||||
Assert.Equal(2 + Width, lead.Start!.Value.X, 9);
|
||||
Assert.Equal(2 + Width, lead.End.X, 9);
|
||||
Assert.Equal(2 + Chamfer, lead.End.Y, 9);
|
||||
var cuts = motions.Where(IsCut).ToList();
|
||||
Assert.Equal(2 + Width, cuts[0].End.X, 9);
|
||||
Assert.Equal(2 + Height - Chamfer, cuts[0].End.Y, 9);
|
||||
Assert.Equal(0, cuts[^1].End.DistanceTo(lead.End), 9);
|
||||
Assert.DoesNotContain(motions, m => m.Layer == LayerType.Leadout);
|
||||
Assert.False(CommonLineCuts.IsSinglePass(result.ProposedOrder[0].CopyProgram()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_CutsEachMembersHolesThenItsSharedCutsThenItsRemainder()
|
||||
public void Row_CutsEachMembersHolesThenItsOutside_StartingOnItsCommonLine()
|
||||
{
|
||||
var plate = Row(3, hole: true);
|
||||
|
||||
@@ -113,20 +126,34 @@ public class CommonLinePlanningTests
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
// The group is cut contiguously, owners before partners.
|
||||
Assert.Equal(plate.Parts, result.ProposedOrder.Select(p => p.SourcePart));
|
||||
Assert.DoesNotContain(result.ProposedOrder.SelectMany(Motions), m => m.Layer == LayerType.CommonLine);
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var motions = Motions(result.ProposedOrder[i]);
|
||||
var hole = motions.ToList().FindLastIndex(m => IsCut(m) && !m.Curve.IsLine);
|
||||
var shared = motions.ToList().FindIndex(m => m.Layer == LayerType.CommonLine);
|
||||
var remainder = motions.ToList().FindIndex(m => IsCut(m) && m.Curve.IsLine);
|
||||
Assert.True(hole >= 0, $"Part {i + 1} cuts no hole.");
|
||||
Assert.True(remainder > hole, $"Part {i + 1} cuts its remainder before its hole.");
|
||||
var motions = Motions(result.ProposedOrder[i]).ToList();
|
||||
var hole = motions.FindLastIndex(m => IsCut(m) && !m.Curve.IsLine);
|
||||
var outside = motions.FindIndex(m => IsCut(m) && m.Curve.IsLine);
|
||||
Assert.True(hole >= 0 && outside > hole, $"Part {i + 1} does not cut its hole before its outside.");
|
||||
var lead = motions[outside - 1];
|
||||
Assert.Equal(LayerType.Leadin, lead.Layer);
|
||||
if (i < 2)
|
||||
Assert.True(shared > hole && shared < remainder, $"Part {i + 1} does not cut hole, shared cut, remainder in order.");
|
||||
{
|
||||
// Entered from below the notch along its right edge, the common line it cuts.
|
||||
var edge = 2 + (i + 1) * Width + i * Kerf;
|
||||
Assert.Equal(edge, lead.Start!.Value.X, 9);
|
||||
Assert.Equal(edge, motions[outside].End.X, 9);
|
||||
Assert.Equal(2 + Height - Chamfer, motions[outside].End.Y, 9);
|
||||
}
|
||||
else
|
||||
Assert.Equal(-1, shared); // The last member owns no shared cut.
|
||||
Assert.Equal(Centerline(1), lead.Start!.Value.X, 9); // The last only cuts a remainder.
|
||||
}
|
||||
Assert.Equal(2, result.ProposedOrder.SelectMany(Motions).Count(m => m.Layer == LayerType.CommonLine));
|
||||
// The middle part stops at the kerf its left neighbour opened, then cuts the stretch below
|
||||
// from that kerf, ending where it began, beside the part it is cut before.
|
||||
var middle = Motions(result.ProposedOrder[1]).ToList();
|
||||
var entries = middle.Where((m, i) => m.Layer == LayerType.Leadin && i + 1 < middle.Count
|
||||
&& IsCut(middle[i + 1]) && middle[i + 1].Curve.IsLine).ToList();
|
||||
Assert.Equal(2, entries.Count);
|
||||
var start = entries[0].End;
|
||||
Assert.Equal(0, middle.Last(IsCut).End.DistanceTo(start), 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -136,60 +163,59 @@ public class CommonLinePlanningTests
|
||||
var result = Plan(plate);
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
|
||||
foreach (var proposal in result.ProposedOrder)
|
||||
{
|
||||
var motions = Motions(proposal);
|
||||
var lead = motions.Last(m => m.Layer == LayerType.Leadin);
|
||||
// Pierced on the centerline (nothing left to pierce), entering the member's own edge.
|
||||
Assert.Equal(Centerline(0), lead.Start!.Value.X, 9);
|
||||
var edge = ReferenceEquals(proposal.SourcePart, plate.Parts[0]) ? 2 + Width : 2 + Width + Kerf;
|
||||
Assert.Equal(edge, lead.End.X, 9);
|
||||
// The remainder ends at the other end of its shared span, on the member's edge.
|
||||
var last = motions.Last(IsCut);
|
||||
Assert.Equal(edge, last.End.X, 9);
|
||||
}
|
||||
var motions = Motions(result.ProposedOrder[1]);
|
||||
var lead = motions.Last(m => m.Layer == LayerType.Leadin);
|
||||
// Pierced on the centerline (nothing left to pierce), entering the partner's own edge.
|
||||
Assert.Equal(Centerline(0), lead.Start!.Value.X, 9);
|
||||
Assert.Equal(2 + Width + Kerf, lead.End.X, 9);
|
||||
// The remainder ends at the other end of its shared span, on the partner's edge.
|
||||
Assert.Equal(2 + Width + Kerf, motions.Last(IsCut).End.X, 9);
|
||||
Assert.True(CommonLineCuts.IsSinglePass(result.ProposedOrder[1].CopyProgram()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Members_CutTheirPerimeterExactlyOnce_SharedSpansByTheSharedCut()
|
||||
public void Members_CutTheirPerimeterExactlyOnce_SharedSpansByTheFirstPart()
|
||||
{
|
||||
var plate = Row(2);
|
||||
var part = plate.Parts[0];
|
||||
var spans = CommonLineGeometry.Check(plate.CommonLines[0]).Spans;
|
||||
var marked = CommonLineCutting.MarkSharedSpans(part.BaseDrawing.Program, part.Location, spans, Kerf);
|
||||
var material = LeadMaterialSnapshot.Capture(marked, part.Location, default, sharedSpans: true);
|
||||
Assert.True(material.IsComplete, material.Reason);
|
||||
|
||||
var order = CommonLineCutting.Order(plate.CommonLines[0], CommonLineGeometry.Check(plate.CommonLines[0]));
|
||||
var result = Plan(plate);
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
var planned = ExecutionMotionReader.Read(result.ProposedOrder[0].CopyProgram(), part.Location, null, default);
|
||||
Assert.True(ContourProgramVerifier.Verify(planned, material, null, default));
|
||||
|
||||
// Cutting the whole perimeter would cut the shared span a second time.
|
||||
var whole = ExecutionMotionReader.Read(part.BaseDrawing.Program, part.Location, null, default);
|
||||
Assert.False(ContourProgramVerifier.Verify(whole, material, null, default));
|
||||
foreach (var proposal in result.ProposedOrder)
|
||||
{
|
||||
var part = proposal.SourcePart;
|
||||
var marked = CommonLineCutting.MarkSharedSpans(part.BaseDrawing.Program, part.Location,
|
||||
order.PathsCutBefore(part).ToArray(), Kerf);
|
||||
var material = LeadMaterialSnapshot.Capture(marked, part.Location, default, sharedSpans: true);
|
||||
Assert.True(material.IsComplete, material.Reason);
|
||||
var planned = ExecutionMotionReader.Read(proposal.CopyProgram(), part.Location, null, default);
|
||||
Assert.True(ContourProgramVerifier.Verify(planned, material, null, default));
|
||||
if (ReferenceEquals(part, plate.Parts[1]))
|
||||
{
|
||||
// The partner cutting its whole perimeter would cut the common line a second time.
|
||||
var whole = ExecutionMotionReader.Read(part.BaseDrawing.Program, part.Location, null, default);
|
||||
Assert.False(ContourProgramVerifier.Verify(whole, material, null, default));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharedCutLeadBlockedAtBothEnds_RefusesNamingTheSpan()
|
||||
public void CommonLineStartBlocked_RefusesNamingTheLine()
|
||||
{
|
||||
var plate = Row(2, x: 2, y: 2);
|
||||
// Strips just past both notches stop the axial lead-ins and lead-outs.
|
||||
foreach (var y in new[] { 2 - 0.3, 2 + Height + 0.1 })
|
||||
{
|
||||
var strip = new Program();
|
||||
strip.Codes.Add(new RapidMove(0, 0));
|
||||
strip.Codes.Add(new LinearMove(0, 0.2));
|
||||
strip.Codes.Add(new LinearMove(4, 0.2));
|
||||
strip.Codes.Add(new LinearMove(4, 0));
|
||||
strip.Codes.Add(new LinearMove(0, 0));
|
||||
plate.Parts.Add(new Part(new Drawing("strip", strip), new Vector(Centerline(0) - 2, y)));
|
||||
}
|
||||
// A strip just below the notch stops the straight lead-in up the shared edge.
|
||||
var strip = new Program();
|
||||
strip.Codes.Add(new RapidMove(0, 0));
|
||||
strip.Codes.Add(new LinearMove(0, 0.2));
|
||||
strip.Codes.Add(new LinearMove(4, 0.2));
|
||||
strip.Codes.Add(new LinearMove(4, 0));
|
||||
strip.Codes.Add(new LinearMove(0, 0));
|
||||
plate.Parts.Add(new Part(new Drawing("strip", strip), new Vector(Centerline(0) - 2, 2 - 0.3)));
|
||||
|
||||
var result = Plan(plate, Parameters(leadIn: 0.5));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
|
||||
Assert.Contains(result.Findings, f => f.Message.Contains("No lead fits the common line between parts 1 and 2"));
|
||||
Assert.Contains(result.Findings, f => f.Message.Contains("No lead fits the start of the common line between parts 1 and 2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -236,7 +262,9 @@ public class CommonLinePlanningTests
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
|
||||
Assert.All(plate.Parts.Take(2), p => Assert.True(CommonLineCuts.IsSinglePass(p.Program)));
|
||||
// The first part cuts a normal closed perimeter; the others cut remainders.
|
||||
Assert.False(CommonLineCuts.IsSinglePass(plate.Parts[0].Program));
|
||||
Assert.All(plate.Parts.Skip(1), p => Assert.True(CommonLineCuts.IsSinglePass(p.Program)));
|
||||
Assert.Equal(utilization, plate.Utilization(), 12);
|
||||
Assert.Equal(nested, drawing.Quantity.Nested);
|
||||
Assert.Equal(overlaps, plate.HasOverlappingParts(out _));
|
||||
@@ -263,7 +291,7 @@ public class CommonLinePlanningTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePost_ReportsACommonLineLeftUncut()
|
||||
public void PrePost_ReportsACommonLineCutTwice()
|
||||
{
|
||||
var nest = new Nest("N26-295");
|
||||
var plate = Row(2);
|
||||
@@ -277,7 +305,7 @@ public class CommonLinePlanningTests
|
||||
|
||||
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
|
||||
f => f.Kind == PostVerificationKind.CommonLine);
|
||||
Assert.Contains("not cut once", finding.Message);
|
||||
Assert.Contains("cut more than once", finding.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -356,11 +384,11 @@ public class CommonLinePlanningTests
|
||||
var perimeter = 2 * (Width + Height) - 8 * Chamfer + 4 * Chamfer * System.Math.Sqrt(2);
|
||||
var alone = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, single));
|
||||
Assert.True(Motions(alone).Where(IsCut).Sum(m => m.Length) < perimeter - 0.05, "The lone part is not tabbed.");
|
||||
foreach (var member in result.ProposedOrder.Where(p => !ReferenceEquals(p.SourcePart, single)))
|
||||
{
|
||||
var cut = Motions(member).Where(IsCut).Sum(m => m.Length);
|
||||
Assert.Equal(perimeter - (Height - 2 * Chamfer), cut, 6);
|
||||
}
|
||||
// The first member cuts its whole perimeter, common line included; its partner the rest.
|
||||
var first = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, plate.Parts[0]));
|
||||
var partner = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, plate.Parts[1]));
|
||||
Assert.Equal(perimeter, Motions(first).Where(IsCut).Sum(m => m.Length), 6);
|
||||
Assert.Equal(perimeter - (Height - 2 * Chamfer), Motions(partner).Where(IsCut).Sum(m => m.Length), 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -399,7 +427,7 @@ public class CommonLinePlanningTests
|
||||
Assert.True(read.Plates[1].CutCommonLinesTwice);
|
||||
var parts = read.Plates[0].Parts;
|
||||
Assert.Equal(Motions(plate.Parts[0]).Select(m => (m.Layer, m.End)), Motions(parts[0]).Select(m => (m.Layer, m.End)));
|
||||
Assert.True(CommonLineCuts.IsSinglePass(parts[0].Program));
|
||||
Assert.False(CommonLineCuts.IsSinglePass(parts[0].Program));
|
||||
Assert.True(CommonLineCuts.IsSinglePass(parts[1].Program));
|
||||
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(read).Findings, f => f.PlateNumber == 1
|
||||
&& f.Kind is PostVerificationKind.CommonLine or PostVerificationKind.RapidCrossing);
|
||||
|
||||
@@ -9,9 +9,10 @@ using OpenNest.Posts.GravographIS;
|
||||
namespace OpenNest.Tests.CommonLines;
|
||||
|
||||
/// <summary>
|
||||
/// Posting common lines cut once (docs/common-line-cutting.md): the CI Fiber post writes each
|
||||
/// shared cut in the cut-off form with its lead-in and each remainder in the contour form,
|
||||
/// compensated on its perimeter's side; posts that cannot write them refuse the nest.
|
||||
/// Posting common lines cut once (docs/common-line-cutting.md): the part cut first is a normal
|
||||
/// compensated perimeter that starts on its common line; the CI Fiber post writes each remainder
|
||||
/// in the contour form, compensated on its perimeter's side; posts that cannot write remainders
|
||||
/// refuse the nest.
|
||||
/// </summary>
|
||||
public class CommonLinePostTests
|
||||
{
|
||||
@@ -35,7 +36,7 @@ public class CommonLinePostTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CIFiber_WritesTheSharedCutUncompensatedAndEachRemainderOnItsPerimetersSide()
|
||||
public void CIFiber_WritesTheCommonLineAsThePerimetersStart_AndTheRemainderOnItsPerimetersSide()
|
||||
{
|
||||
var nest = PlannedPair();
|
||||
var post = new CIFiberPostProcessor(new CIFiberPostConfig { ConfigurationName = "CI FIBER 4020" });
|
||||
@@ -43,9 +44,10 @@ public class CommonLinePostTests
|
||||
|
||||
post.Post(nest, stream);
|
||||
|
||||
// Part 1 cuts the shared line on the centerline (x 8.006), no G41/G42, entered and left
|
||||
// through the notches; then its remainder, pierced in that kerf and compensated G42 as its
|
||||
// counterclockwise perimeter would be. Part 2's remainder enters the same kerf at its other end.
|
||||
// Part 1 enters below the notch straight up its shared edge (x 8, compensated G42 onto the
|
||||
// centerline), cuts the common line and carries on round its perimeter, closing where it
|
||||
// began. Part 2's remainder is pierced in that kerf and compensated G42 as its
|
||||
// counterclockwise perimeter would be.
|
||||
const string golden = """
|
||||
( Sheet number - 1 )
|
||||
( Part #1 )
|
||||
@@ -54,20 +56,12 @@ public class CommonLinePostTests
|
||||
N1:
|
||||
/L "L0"
|
||||
V.E.R3=1
|
||||
G0X8.006Y2.13
|
||||
/L "L4"
|
||||
G1X8.006Y2.38
|
||||
/L "L6"
|
||||
G1X8.006Y14.06
|
||||
/L "ZHSOFF"
|
||||
N2:
|
||||
/L "L0"
|
||||
V.E.R3=2
|
||||
G0X8.006Y13.81
|
||||
G0X8Y2.13
|
||||
/L "L4"
|
||||
G42
|
||||
G1X8Y14.06
|
||||
G1X8Y2.38
|
||||
/L "L6"
|
||||
G1X8Y14.06
|
||||
G1X7.62Y14.44
|
||||
G1X2.38Y14.44
|
||||
G1X2Y14.06
|
||||
@@ -80,9 +74,9 @@ public class CommonLinePostTests
|
||||
( Part #2 )
|
||||
( PART:PT42 )
|
||||
V.E.R4=2
|
||||
N3:
|
||||
N2:
|
||||
/L "L0"
|
||||
V.E.R3=3
|
||||
V.E.R3=2
|
||||
G0X8.006Y2.63
|
||||
/L "L4"
|
||||
G42
|
||||
@@ -147,7 +141,7 @@ public class CommonLinePostTests
|
||||
|
||||
var refused = Assert.Throws<InvalidOperationException>(() =>
|
||||
CommonLineCuts.EnsurePostable(nest, new NoCommonLinePost()));
|
||||
Assert.Contains("Plate 1, part 1 cuts a common line once", refused.Message);
|
||||
Assert.Contains("Plate 1, part 2 cuts a common line once", refused.Message);
|
||||
CommonLineCuts.EnsurePostable(nest, new CIFiberPostProcessor());
|
||||
|
||||
// Cut twice, nothing is single-pass, so any post may write it.
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace OpenNest.Tests.CommonLines;
|
||||
/// <summary>
|
||||
/// A Z-shaped common line (docs/common-line-cutting.md): two stepped bars, the second turned half
|
||||
/// a turn, interlock one kerf apart along three straight legs. The legs' spans chain at their
|
||||
/// corners into one shared path, cut once as one open centerline cut.
|
||||
/// corners into one shared path. The part cut first starts its perimeter on it, entered along its
|
||||
/// first leg from the scrap, and cuts the whole Z straight into the rest of its perimeter.
|
||||
/// </summary>
|
||||
public class CommonLineZTests
|
||||
{
|
||||
@@ -99,33 +100,38 @@ public class CommonLineZTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_CutsTheZOnceAsOneOpenPath()
|
||||
public void Plan_TheFirstPartStartsOnTheZAndRunsStraightIntoItsPerimeter()
|
||||
{
|
||||
var (plate, _, _) = Pair();
|
||||
var (plate, a, _) = Pair();
|
||||
|
||||
var result = Plan(plate);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.DoesNotContain(result.ProposedOrder.SelectMany(Motions), m => m.Layer == LayerType.CommonLine);
|
||||
var owner = result.ProposedOrder[0];
|
||||
Assert.Same(a, owner.SourcePart);
|
||||
var motions = Motions(owner).ToList();
|
||||
var shared = motions.Where(m => m.Layer == LayerType.CommonLine).ToList();
|
||||
Assert.Equal(3, shared.Count);
|
||||
// One pierce: the three legs are cut in a row, each starting where the last ended.
|
||||
var first = motions.IndexOf(shared[0]);
|
||||
Assert.Equal(LayerType.Leadin, motions[first - 1].Layer);
|
||||
for (var i = 1; i < shared.Count; i++)
|
||||
{
|
||||
Assert.Same(shared[i], motions[first + i]);
|
||||
Assert.Equal(0, shared[i - 1].End.DistanceTo(shared[i].Curve.Start), 9);
|
||||
}
|
||||
Assert.Equal(Step + 2 * (Low - K / 2), shared.Sum(m => m.Length), 9);
|
||||
// The partner cuts none of it.
|
||||
Assert.DoesNotContain(Motions(result.ProposedOrder[1]), m => m.Layer == LayerType.CommonLine);
|
||||
// One lead-in, up the Z's first leg from below, then its three legs in a row.
|
||||
var lead = Assert.Single(motions, m => m.Layer == LayerType.Leadin);
|
||||
Assert.Equal(2, lead.Start!.Value.X, 9);
|
||||
Assert.Equal(2, lead.End.X, 9);
|
||||
Assert.Equal(2, lead.End.Y, 9);
|
||||
var cuts = motions.Where(IsCut).ToList();
|
||||
Assert.Equal(new Vector(2, 2 + Low), cuts[0].End);
|
||||
Assert.Equal(new Vector(2 + Step, 2 + Low), cuts[1].End);
|
||||
Assert.Equal(new Vector(2 + Step, 2 + High), cuts[2].End);
|
||||
Assert.Equal(0, cuts[^1].End.DistanceTo(lead.End), 9);
|
||||
Assert.DoesNotContain(motions, m => m.Layer == LayerType.Leadout);
|
||||
// The partner cuts only its remainder.
|
||||
var partner = Motions(result.ProposedOrder[1]).Where(IsCut).Sum(m => m.Length);
|
||||
Assert.True(partner < 2 * (Length + High) - (Step + 2 * Low) + 2 * K);
|
||||
}
|
||||
|
||||
private static bool IsCut(ExecutionMotion m) => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display;
|
||||
|
||||
[Fact]
|
||||
public void Members_CutTheirPerimeterExactlyOnce_TheZByTheSharedCut()
|
||||
public void Members_CutTheirPerimeterExactlyOnce_TheZByTheFirstPart()
|
||||
{
|
||||
var (plate, _, _) = Pair();
|
||||
var order = CommonLineCutting.Order(plate.CommonLines[0], CommonLineGeometry.Check(plate.CommonLines[0]));
|
||||
@@ -135,11 +141,12 @@ public class CommonLineZTests
|
||||
foreach (var proposal in result.ProposedOrder)
|
||||
{
|
||||
var part = proposal.SourcePart;
|
||||
var marked = CommonLineCutting.MarkSharedSpans(part.Program, part.Location, order.PathsOf(part).ToArray(), K);
|
||||
// The Z is one tab run on each member: its legs join at the member's own corners.
|
||||
var cutBefore = order.PathsCutBefore(part).ToArray();
|
||||
var marked = CommonLineCutting.MarkSharedSpans(part.Program, part.Location, cutBefore, K);
|
||||
// The partner's Z is one tab run: its legs join at its own corners.
|
||||
var layers = marked.Codes.OfType<LinearMove>().Select(m => m.Layer).ToList();
|
||||
Assert.Single(layers.Where((layer, i) => layer == LayerType.Tab
|
||||
&& layers[(i + layers.Count - 1) % layers.Count] != LayerType.Tab));
|
||||
Assert.Equal(cutBefore.Length, layers.Where((layer, i) => layer == LayerType.Tab
|
||||
&& layers[(i + layers.Count - 1) % layers.Count] != LayerType.Tab).Count());
|
||||
var material = LeadMaterialSnapshot.Capture(marked, part.Location, default, sharedSpans: true);
|
||||
Assert.True(material.IsComplete, material.Reason);
|
||||
var planned = ExecutionMotionReader.Read(proposal.CopyProgram(), part.Location, null, default);
|
||||
|
||||
@@ -73,23 +73,25 @@ uses the Cincinnati machine-sample convention, not a generic TF5200 laser API:
|
||||
A [common line cut once](common-line-cutting.md#cutting-each-shared-line-once) posts in plate
|
||||
order, member by member, each member's holes first:
|
||||
|
||||
- **Shared cut** (`CommonLine` motions): the cut-off form with a real lead-in. `/L "L0"`, restart
|
||||
number, rapid to the pierce on the span's axis, `/L "L4"`, the straight lead-in `G1`, `/L "L6"`,
|
||||
the centerline `G1`, the straight lead-out `G1` when there is one, and `/L "ZHSOFF"`. No
|
||||
`G41`/`G42` is selected: the line is the beam centerline between the two parts.
|
||||
- **Owner's outside**: the part that cuts a common line starts its outside on it. With no span
|
||||
cut before it, it is a normal closed perimeter (`G41`/`G42` from its travel), whose straight
|
||||
lead-in runs up the shared edge's axis, so the common line is its first cut.
|
||||
- **Remainder** (an open contour after a `COMMON LINE REMAINDER` comment): the normal contour
|
||||
form with the perimeter macro, `/L "L4"`, `G41` for a clockwise perimeter or `G42` for a
|
||||
counterclockwise one (the side its whole perimeter would get), the straight lead-in that starts
|
||||
in the shared kerf, `/L "L6"`, the open path and `/L "ZHSOFF"`. A member's closed contours are
|
||||
all holes; the post picks no perimeter among them.
|
||||
counterclockwise one (the side its whole perimeter would get), the straight lead-in, `/L "L6"`,
|
||||
the open path and `/L "ZHSOFF"`. A member's closed contours are then all holes; the post picks
|
||||
no perimeter among them. An owner whose outside stops in an earlier member's kerf writes each
|
||||
of its pieces this way.
|
||||
- **Shared cut** (`CommonLine` motions, programs planned before the owner cut its own edge): the
|
||||
cut-off form with a real lead-in, no `G41`/`G42`.
|
||||
|
||||
**Unconfirmed on the machine** (release gate, see [common-line cutting](common-line-cutting.md)):
|
||||
|
||||
- compensation start-up on a remainder whose lead-in starts in void (the kerf the shared cut
|
||||
opened) and runs along it;
|
||||
- compensation start-up on a remainder whose lead-in starts in void (the kerf the owner's cut
|
||||
opened) and runs along it, and on the owner's lead-in collinear with its shared edge;
|
||||
- compensation cancel where a remainder ends in that kerf, and that the next contour starts
|
||||
uncompensated, since the post writes no `G40` (the macros cancel compensation);
|
||||
- whether `L4` runs correctly before an uncompensated shared cut, as for cut-offs.
|
||||
- whether `L4` runs correctly before an older program's uncompensated shared cut, as for cut-offs.
|
||||
|
||||
Verify on a 3/16 stainless coupon pair on the CI FIBER 4020 and record the result here.
|
||||
|
||||
@@ -153,7 +155,7 @@ transforms/formatting, lead-in rejection,
|
||||
cut-off sequencing and output, table bounds, suppression/scribe handling, and
|
||||
multi-sheet output in both modes.
|
||||
[Common-line post tests](../OpenNest.Tests/CommonLines/CommonLinePostTests.cs) hold a golden
|
||||
program for a planned two-part pair (shared cut and both remainders), the remainders'
|
||||
program for a planned two-part pair (the owner's perimeter starting on the common line, and the partner's remainder), the remainders'
|
||||
compensation side for either winding, and the refusals of posts without support.
|
||||
[Sample regression tests](../OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs)
|
||||
post a nest rebuilt from a Cincinnati machine sample, whose parts carry the
|
||||
|
||||
+48
-38
@@ -46,44 +46,52 @@ planned member's open program changes no utilization, quantity, overlap or spaci
|
||||
order that cuts the partner first is a `ConstraintConflict` naming "the common-line partner").
|
||||
The tour treats each such pair like a nested part and its host, so a group is cut as one run.
|
||||
A rule the search learns later may still separate members; the dependency always holds.
|
||||
- **The common line is the owner's own edge.** Each member cuts its outside compensated like any
|
||||
perimeter. Because the parts lie exactly one kerf apart, the owner's compensated shared edge
|
||||
runs on the kerf centerline: that cut is the common line, and it opens the partner's edge too.
|
||||
No cut is made without compensation. A Z or L path is cut through its corners as the owner's
|
||||
own edges; at each corner the kerf leaves the member on the outside of the turn a fillet of
|
||||
about half a kerf's radius where its nominal corner is sharp.
|
||||
- **Each member is cut** as: its holes (and break cuts and tabbed cutouts, as for any part), then
|
||||
the shared cuts it owns, then its **remainder**: its perimeter minus every span it shares, in
|
||||
the perimeter's own direction, as one open piece per stretch between spans. Shared cuts are
|
||||
never all cut first: each is cut where the route passes its owner. A Z or L path is one shared
|
||||
cut, owned and cut as a whole.
|
||||
- **Shared cut:** an open cut on the centerline (`LayerType.CommonLine`), cut without kerf
|
||||
compensation. It is pierced in scrap on its first leg's axis, the external lead-in's reach
|
||||
before the path, and entered with a straight lead-in through the end notch; a Z or L path is
|
||||
then cut through its corners in one pass, one `CommonLine` move per leg. It leaves the same way
|
||||
at the far end, along its last leg, with a straight lead-out as long as the external lead-out's
|
||||
reach (none with no lead-out). At each corner the kerf leaves the member on the outside of the
|
||||
turn a fillet of half a kerf's radius where its nominal corner is sharp; the member on the
|
||||
inside keeps a sharp corner. Capture tries each end in turn: the lead-in, cut and lead-out may touch no
|
||||
material, and the pierce keeps the pierce zone. When no end fits, planning refuses with
|
||||
`UnsupportedGeometry` naming both parts and the span. Leads are never shortened.
|
||||
- **Remainder:** each piece starts where a shared span ends. It is pierced in that span's kerf,
|
||||
on the centerline, the external lead-in's reach back along the span, and enters along a straight
|
||||
lead-in to the piece's start, so compensation starts inside the kerf the shared cut opened (for
|
||||
a Z or L path, the kerf of its last leg before the piece); the
|
||||
pierce meets nothing. The comment `COMMON LINE REMAINDER CW` (or `CCW`) after its rapid names
|
||||
the perimeter's winding, so posts compensate it on the perimeter's side. It has no lead-out: it
|
||||
ends in the kerf at the next span. A span shorter than the lead-in's reach, or an external
|
||||
lead-in with no length, refuses with `UnsupportedGeometry`.
|
||||
- **Accounting.** The planner marks each member's shared spans on its owned clean copy as `Tab`
|
||||
spans (`CommonLineCutting.MarkSharedSpans`): material the member never cuts. At a path's corner
|
||||
each leg's span runs on to the member's own vertex there, so a Z or L is one run of tabs. Replay's contour
|
||||
accounting proves each stretch between them is cut whole, once, as for
|
||||
[tabbed cutouts](tabbed-cutouts.md#verification), and refuses a member that also cuts a span.
|
||||
- **Rapids.** A shared cut is never a rapid obstacle and takes no part in the open-cut retention
|
||||
proof. When a member's program ends, its remainder pieces, joined across its spans, become one
|
||||
obstacle: the part is cut free then.
|
||||
its outside, in the perimeter's own direction, never cutting a span an earlier member cut:
|
||||
- **Starting on its common line.** A member that owns a shared cut starts its outside there:
|
||||
pierced in scrap the external lead-in's reach back along the axis of the edge on which its
|
||||
travel enters the path, a straight lead-in to that edge's start vertex, then the common line
|
||||
and on round its perimeter with no lead-out. With no span cut before it (the group's first
|
||||
member), that is its whole perimeter, closing where it began: a normal closed contour. Otherwise
|
||||
it stops in the first earlier member's kerf it reaches, and its other stretches follow as
|
||||
remainder pieces, the stretch before its start last, ending at it, beside the partner it is
|
||||
cut before. Capture tries the start of each path it owns: the lead-in may touch no material and
|
||||
the pierce keeps the pierce zone. When none fits, planning refuses with `UnsupportedGeometry`
|
||||
naming both parts and the line. Leads are never shortened, and an external lead-in with no
|
||||
length refuses.
|
||||
- **Remainder:** a member that owns nothing cuts only its perimeter minus the spans earlier
|
||||
members cut, as one open piece per stretch between them. Each piece starts where such a span
|
||||
ends. It is pierced in that span's kerf, on the centerline, the external lead-in's reach back
|
||||
along the span, and enters along a straight lead-in to the piece's start, so compensation
|
||||
starts inside the opened kerf (for a Z or L path, the kerf of its last leg before the piece);
|
||||
the pierce meets nothing. It has no lead-out: it ends in the kerf at the next span. A span
|
||||
shorter than the lead-in's reach refuses with `UnsupportedGeometry`.
|
||||
- Every open piece (a remainder, or a starting stretch that stops in a kerf) is preceded by the
|
||||
comment `COMMON LINE REMAINDER CW` (or `CCW`), naming the perimeter's winding, so posts
|
||||
compensate it on the perimeter's side.
|
||||
- **Accounting.** The planner marks the spans earlier members cut on each member's owned clean
|
||||
copy as `Tab` spans (`CommonLineCutting.MarkSharedSpans`): material the member never cuts. At a
|
||||
path's corner each leg's span runs on to the member's own vertex there, so a Z or L is one run
|
||||
of tabs. Replay's contour accounting proves each stretch between them is cut exactly once, as
|
||||
for [tabbed cutouts](tabbed-cutouts.md#verification): its pieces tile it with no gap or overlap
|
||||
(two pieces where a member starts on its common line inside a stretch), and it refuses a member
|
||||
that also cuts a span.
|
||||
- **Rapids.** When a member's program ends, its pieces, joined across the spans earlier members
|
||||
cut, become one obstacle: the part is cut free then. Older programs' uncompensated `CommonLine`
|
||||
centerline cuts are never rapid obstacles.
|
||||
- **Refusals.** A broken group, and a locked member or one a caller left out of the plan, cannot
|
||||
be cut once: rejoin, unlock or include it, or cut the plate's lines twice. A drawing pin on a
|
||||
member's outside is not applied (a warning makes the plan UNVERIFIED); pins on its holes are.
|
||||
Members are never support-tabbed. Remainder pierces in the kerf are held to the pierce zone like
|
||||
any other, so a pierce diameter wider than the kerf refuses them.
|
||||
- **Apply** installs each member's program (holes, shared cuts, remainder); nothing is added to
|
||||
the plate.
|
||||
- **Apply** installs each member's program (holes, then its outside); nothing is added to the
|
||||
plate.
|
||||
|
||||
### Cut common lines twice
|
||||
|
||||
@@ -181,14 +189,16 @@ Auto Nest, the server's nesting worker and Fill Area can place parts on common l
|
||||
- a group whose kerf differs from the plate's cutting parameters' kerf (when the plate has
|
||||
them) by more than `GapTolerance`. A group at the wrong kerf would leave a web or cut a sliver.
|
||||
Rejoin it at the current kerf;
|
||||
- a lead-in, lead-out or shared cut of a member that touches or enters another member's
|
||||
- a lead-in, lead-out or (older programs') centerline shared cut of a member that touches or enters another member's
|
||||
material. A lead on a shared edge would cut the partner one kerf away. An unreadable lead
|
||||
check is incomplete, never clear;
|
||||
- on a plate that cuts its lines once, a span no member's program cuts on its centerline (for
|
||||
example a plate never planned, which would cut it twice), a span cut more than once, and a
|
||||
span cut after the partner whose remainder ends in it;
|
||||
- on a plate set to cut its lines twice, a member program that still cuts one once.
|
||||
- The rapid check treats members' remainders and shared cuts as planning does.
|
||||
- on a plate that cuts its lines once, a span no member's program cuts, a span cut more than
|
||||
once (both members cutting their edge along it, as a plate not planned since the parts were
|
||||
joined does), and a span cut after the partner whose remainder ends in it. A span counts as cut
|
||||
by a member whose compensated edge covers it half a kerf off the centerline, or by an older
|
||||
program's uncompensated centerline cut;
|
||||
- on a plate set to cut its lines twice, a member program that still cuts one once (a remainder).
|
||||
- The rapid check treats members' remainders as planning does.
|
||||
|
||||
## Posting
|
||||
|
||||
|
||||
@@ -130,11 +130,12 @@ their canonical content, like the other nesting settings, so it never changes a
|
||||
|
||||
A plate's optional `cutCommonLinesTwice` (`Plate.CutCommonLinesTwice`) is `true` when its
|
||||
common lines are cut twice; it is omitted when false, the default, in which Plan Cutting cuts
|
||||
each shared span once. A program planned that way saves its shared cuts as motions tagged
|
||||
`:COMMONLINE` and puts a `COMMON LINE REMAINDER CW` or `CCW` comment after the rapid into each
|
||||
remainder piece. Versions without single-pass support read those motions as ordinary cuts,
|
||||
which a post would cut with compensation, so such a nest must not be posted from an older
|
||||
version.
|
||||
each shared span once. A program planned that way puts a `COMMON LINE REMAINDER CW` or `CCW`
|
||||
comment after the rapid into each remainder piece; the part that cuts the common line saves an
|
||||
ordinary compensated perimeter. Programs planned by earlier versions may also hold uncompensated
|
||||
shared cuts as motions tagged `:COMMONLINE`, which still read and post. Versions without
|
||||
single-pass support read a remainder as an ordinary open cut, so such a nest must not be posted
|
||||
from an older version.
|
||||
|
||||
## Plate cutting parameters
|
||||
|
||||
|
||||
@@ -57,8 +57,10 @@ and before the contours, never in single-contour probes:
|
||||
## Verification
|
||||
|
||||
Plan Cutting certifies tabbed cutouts. Its contour accounting
|
||||
(`ContourProgramVerifier`) requires each piece to start exactly where a span between tabs
|
||||
starts and to cover that span whole, once; the ring counts as cut when every span is.
|
||||
(`ContourProgramVerifier`) requires each piece to follow one span between tabs without
|
||||
crossing a tab, and the pieces to tile every span with no gap or overlap; the ring counts as cut
|
||||
when every span is. A tabbed cutout's pieces each cover their span whole; only a
|
||||
[common-line](common-line-cutting.md) member that starts on its common line cuts one span in two.
|
||||
Lead validation treats a piece's start like any contour joint. The released-contour check
|
||||
proves the open pieces retain their slug with the same open-cut proof pre-post review uses
|
||||
(see [pre-post verification](post-verification.md)); planning still refuses its own tabs
|
||||
|
||||
Reference in new issue
Block a user