mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 03:31:34 -05:00
fix(verify): pass tabbed cutouts that provably retain their slug
A cutout cut in several pieces around tabs, as PEP programs often are, made pre-post review report every piece's lead-out and the part's open fragments as incomplete. Review now proves such open cuts release nothing: within the part they touch other cuts only at their own ends and form no loop (each closed contour with its leads counting as one point), and leads at open ends stay out of the gap beyond them. Anything else stays incomplete. Plan Cutting keeps refusing to certify its own tabs followed by a lead-out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3672697cb2
commit
6cdda38d35
5 files changed
+332
-30
No files matched your search
@@ -64,11 +64,19 @@ public sealed class ReleasedContourState
|
||||
|
||||
internal void AnalyzeMoves(IReadOnlyList<ExecutionMotion> moves, bool cutoff,
|
||||
List<PostVerificationFinding> findings, int plate, int part, CancellationToken token,
|
||||
Func<int, bool> isFixed = null, List<PostVerificationFinding> fixedFindings = null)
|
||||
Func<int, bool> isFixed = null, List<PostVerificationFinding> fixedFindings = null,
|
||||
bool proveOpenCuts = false)
|
||||
{
|
||||
var contour = new List<PostVerificationGeometry.Curve>();
|
||||
var contourFixed = false;
|
||||
var unfinished = new List<(PostVerificationGeometry.Curve[] Curves, bool Fixed)>();
|
||||
var current = new CutGroup();
|
||||
var unfinished = new List<CutGroup>();
|
||||
// Part-local cuts read only by the open-cut proof below: closed contours, break cuts
|
||||
// and leads no contour owns. Each lead belongs to the cut it enters or leaves.
|
||||
var closed = new List<CutGroup>();
|
||||
var otherOpen = new List<CutGroup>();
|
||||
var loose = new CutGroup();
|
||||
var leadIns = new List<PostVerificationGeometry.Curve>();
|
||||
CutGroup previous = null;
|
||||
var deferred = new List<(List<PostVerificationFinding> Target, int Index, PostVerificationFinding Finding)>();
|
||||
var hasLead = false;
|
||||
var contourNumber = 0;
|
||||
var inBreakCut = false;
|
||||
@@ -90,7 +98,13 @@ public sealed class ReleasedContourState
|
||||
{ Location = move.Curve.Start }, owned);
|
||||
inBreakCut = true;
|
||||
hasLead = false;
|
||||
previous = new CutGroup();
|
||||
previous.Leads.AddRange(leadIns);
|
||||
leadIns.Clear();
|
||||
otherOpen.Add(previous);
|
||||
}
|
||||
if (move.Curve.Length > PostVerificationGeometry.Epsilon)
|
||||
previous.Curves.Add(move.Curve);
|
||||
continue;
|
||||
}
|
||||
inBreakCut = false;
|
||||
@@ -98,6 +112,9 @@ public sealed class ReleasedContourState
|
||||
{
|
||||
Finish();
|
||||
hasLead = false;
|
||||
loose.Leads.AddRange(leadIns);
|
||||
leadIns.Clear();
|
||||
previous = null;
|
||||
if (move.Start is not { } start || start.DistanceTo(move.End) <= PostVerificationGeometry.Epsilon)
|
||||
continue;
|
||||
var reach = new PostVerificationGeometry.Extent(System.Math.Min(start.X, move.End.X),
|
||||
@@ -124,22 +141,30 @@ public sealed class ReleasedContourState
|
||||
else if (move.Layer == LayerType.Leadin)
|
||||
{
|
||||
Finish();
|
||||
previous = null;
|
||||
hasLead |= move.Curve.Length > PostVerificationGeometry.Epsilon;
|
||||
if (move.Curve.Length > PostVerificationGeometry.Epsilon)
|
||||
leadIns.Add(move.Curve);
|
||||
}
|
||||
else if (move.Layer is LayerType.Leadout or LayerType.Scribe)
|
||||
{
|
||||
if (move.Layer == LayerType.Leadout && contour.Count > 0
|
||||
&& !PostVerificationGeometry.Closed(contour)
|
||||
&& move.Curve.Length > PostVerificationGeometry.Epsilon)
|
||||
Add(new(PostVerificationKind.Incomplete, plate, part, null,
|
||||
"A lead-out follows an open cutting contour and may cut through its retention gap. " +
|
||||
"Rapid safety for that contour requires manual review."), owned);
|
||||
if (move.Layer == LayerType.Leadout && move.Curve.Length > PostVerificationGeometry.Epsilon)
|
||||
{
|
||||
// Withheld until the part ends, where the open-cut proof may show it cuts no gap.
|
||||
if (current.Curves.Count > 0 && !PostVerificationGeometry.Closed(current.Curves))
|
||||
Defer(new(PostVerificationKind.Incomplete, plate, part, null,
|
||||
"A lead-out follows an open cutting contour and may cut through its retention gap. " +
|
||||
"Rapid safety for that contour requires manual review."), owned);
|
||||
(previous ?? loose).Leads.Add(move.Curve);
|
||||
}
|
||||
Finish();
|
||||
hasLead = false;
|
||||
if (move.Layer == LayerType.Scribe)
|
||||
previous = null;
|
||||
}
|
||||
else if (move.Curve.Length > PostVerificationGeometry.Epsilon)
|
||||
{
|
||||
if (contour.Count == 0)
|
||||
if (current.Curves.Count == 0)
|
||||
{
|
||||
contourNumber++;
|
||||
if (reportMissingLeadIns && !cutoff && !hasLead)
|
||||
@@ -150,26 +175,37 @@ public sealed class ReleasedContourState
|
||||
// A rapid can pause/reposition without leaving any material gap.
|
||||
// Retain already-cut fragments, but do not turn them into obstacles
|
||||
// until an actually continuous chain closes.
|
||||
var previous = unfinished.FindIndex(chain =>
|
||||
var resumed = unfinished.FindIndex(chain =>
|
||||
chain.Curves[^1].End.DistanceTo(move.Curve.Start) <= PostVerificationGeometry.Epsilon);
|
||||
if (previous >= 0)
|
||||
if (resumed >= 0)
|
||||
{
|
||||
contour.AddRange(unfinished[previous].Curves);
|
||||
contourFixed |= unfinished[previous].Fixed;
|
||||
unfinished.RemoveAt(previous);
|
||||
current = unfinished[resumed];
|
||||
unfinished.RemoveAt(resumed);
|
||||
}
|
||||
current.Leads.AddRange(leadIns);
|
||||
leadIns.Clear();
|
||||
}
|
||||
contour.Add(move.Curve);
|
||||
contourFixed |= owned;
|
||||
current.Curves.Add(move.Curve);
|
||||
current.Fixed |= owned;
|
||||
previous = current;
|
||||
// A completed contour becomes an obstacle immediately, not at part end.
|
||||
if (PostVerificationGeometry.Closed(contour))
|
||||
if (PostVerificationGeometry.Closed(current.Curves))
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
Finish();
|
||||
loose.Leads.AddRange(leadIns);
|
||||
// Open cuts that provably enclose nothing retain all their material, however many
|
||||
// pieces a tabbed cutout was cut in. Otherwise they need manual review. Only pre-post
|
||||
// review asks: planning keeps refusing to certify its own tabs followed by a lead-out.
|
||||
var retained = proveOpenCuts && !cutoff && (unfinished.Count > 1 || deferred.Count > 0)
|
||||
&& unfinished.All(LeadsClearOpenEnds)
|
||||
&& OpenCutsEncloseNothing(closed, unfinished.Concat(otherOpen).Append(loose).ToList(), token);
|
||||
for (var i = deferred.Count - 1; i >= 0 && !retained; i--)
|
||||
deferred[i].Target.Insert(deferred[i].Index, deferred[i].Finding);
|
||||
// Only one open fragment per part can be the planner's own certified tab, so a
|
||||
// further fragment that a fixed emission cut makes this uncertainty that emission's.
|
||||
if (unfinished.Count > 1)
|
||||
if (unfinished.Count > 1 && !retained)
|
||||
Add(new(PostVerificationKind.Incomplete, plate, part, null,
|
||||
"Multiple interrupted/open cutting fragments remain. Their combined cuts may release material; " +
|
||||
"they cannot be assumed to be retained by tabs. Review rapid travel manually."),
|
||||
@@ -183,21 +219,187 @@ public sealed class ReleasedContourState
|
||||
findings.Add(finding);
|
||||
}
|
||||
|
||||
// Records the finding's place among the others, in case it is reported after all.
|
||||
void Defer(PostVerificationFinding finding, bool fixedEmission)
|
||||
{
|
||||
var target = fixedEmission && fixedFindings != null ? fixedFindings : findings;
|
||||
deferred.Add((target, target.Count, finding));
|
||||
}
|
||||
|
||||
void Finish()
|
||||
{
|
||||
if (contour.Count == 0)
|
||||
if (current.Curves.Count == 0)
|
||||
return;
|
||||
// A real uncut gap leaves the contour attached. CuttingParameters can be stale;
|
||||
// no flag or tab configuration is used as evidence of retention.
|
||||
if (!cutoff && PostVerificationGeometry.Closed(contour))
|
||||
obstacles.Add(new(part, contourNumber, contour.ToArray()));
|
||||
if (!cutoff && PostVerificationGeometry.Closed(current.Curves))
|
||||
{
|
||||
obstacles.Add(new(part, contourNumber, current.Curves.ToArray()));
|
||||
closed.Add(current);
|
||||
}
|
||||
else if (!cutoff)
|
||||
unfinished.Add((contour.ToArray(), contourFixed));
|
||||
contour.Clear();
|
||||
contourFixed = false;
|
||||
unfinished.Add(current);
|
||||
current = new CutGroup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether one part's open cuts, with their leads, can release no material. A closed
|
||||
/// contour with its own leads already encloses whatever it releases and counts as one node.
|
||||
/// Open cuts may touch any cut only at their own ends, and must form no loop through those
|
||||
/// nodes or each other: a forest of open paths separates no region of the sheet. Kerf and
|
||||
/// tab strength are not modelled, exactly as for a single open contour.
|
||||
/// </summary>
|
||||
private const double JointTolerance = 1e-5;
|
||||
|
||||
/// <summary>
|
||||
/// Whether every lead at an open end of <paramref name="group"/> stays out of the uncut gap
|
||||
/// beyond it: a lead-out never reaches ahead of the end it leaves, and a lead-in never
|
||||
/// behind the start it enters. A lead that curls into the gap can cut the tab with its kerf
|
||||
/// even where its centerline does not touch the next cut.
|
||||
/// </summary>
|
||||
private static bool LeadsClearOpenEnds(CutGroup group)
|
||||
{
|
||||
var start = group.Curves[0].Start;
|
||||
var end = group.Curves[^1].End;
|
||||
var behindStart = group.Curves[0].Tangent(false) * -1;
|
||||
var aheadOfEnd = group.Curves[^1].Tangent(true);
|
||||
foreach (var lead in group.Leads)
|
||||
{
|
||||
if (Touches(lead, start) && lead.Reach(start, behindStart) > PostVerificationGeometry.Epsilon)
|
||||
return false;
|
||||
if (Touches(lead, end) && lead.Reach(end, aheadOfEnd) > PostVerificationGeometry.Epsilon)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
static bool Touches(PostVerificationGeometry.Curve lead, Vector point) =>
|
||||
lead.Start.DistanceTo(point) <= PostVerificationGeometry.Epsilon
|
||||
|| lead.End.DistanceTo(point) <= PostVerificationGeometry.Epsilon;
|
||||
}
|
||||
|
||||
private static bool OpenCutsEncloseNothing(IReadOnlyList<CutGroup> closed, IReadOnlyList<CutGroup> open,
|
||||
CancellationToken token)
|
||||
{
|
||||
var edges = open.SelectMany(group => group.Curves.Concat(group.Leads)).ToList();
|
||||
var nodes = new DisjointSet(closed.Count + 2 * edges.Count);
|
||||
int Node(int edge, bool end) => closed.Count + 2 * edge + (end ? 1 : 0);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < closed.Count; i++)
|
||||
for (var j = 0; j < i; j++)
|
||||
if (Touch(closed[i], closed[j], token))
|
||||
nodes.Union(i, j);
|
||||
for (var a = 0; a < edges.Count; a++)
|
||||
{
|
||||
var edge = edges[a];
|
||||
if (edge.Start.DistanceTo(edge.End) <= PostVerificationGeometry.Epsilon)
|
||||
return false;
|
||||
for (var g = 0; g < closed.Count; g++)
|
||||
{
|
||||
foreach (var other in closed[g].Curves.Concat(closed[g].Leads))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (edge.Extent.IsClearOf(other.Extent))
|
||||
continue;
|
||||
foreach (var point in Contacts(edge, other))
|
||||
{
|
||||
if (EndAt(edge, point, other) is not { } end)
|
||||
return false;
|
||||
nodes.Union(Node(a, end), g);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var b = a + 1; b < edges.Count; b++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var other = edges[b];
|
||||
if (edge.Extent.IsClearOf(other.Extent))
|
||||
continue;
|
||||
foreach (var point in Contacts(edge, other))
|
||||
{
|
||||
if (EndAt(edge, point, other) is not { } end || EndAt(other, point, edge) is not { } otherEnd)
|
||||
return false;
|
||||
nodes.Union(Node(a, end), Node(b, otherEnd));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or NotSupportedException
|
||||
or ArithmeticException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var forest = new DisjointSet(closed.Count + 2 * edges.Count);
|
||||
for (var a = 0; a < edges.Count; a++)
|
||||
{
|
||||
var start = nodes.Find(Node(a, false));
|
||||
var end = nodes.Find(Node(a, true));
|
||||
if (forest.Find(start) == forest.Find(end))
|
||||
return false;
|
||||
forest.Union(start, end);
|
||||
}
|
||||
return true;
|
||||
|
||||
static IReadOnlyList<Vector> Contacts(PostVerificationGeometry.Curve a, PostVerificationGeometry.Curve b)
|
||||
{
|
||||
var points = a.Contacts(b, out var overlap);
|
||||
if (overlap)
|
||||
throw new ArgumentException("Cuts share a span.");
|
||||
return points;
|
||||
}
|
||||
|
||||
// Native contacts at a near-tangent joint (a lead or line meeting an arc) can land
|
||||
// slightly off the vertex the two cuts share; that is still their shared end.
|
||||
static bool? EndAt(PostVerificationGeometry.Curve curve, Vector point, PostVerificationGeometry.Curve other)
|
||||
{
|
||||
foreach (var end in new[] { false, true })
|
||||
{
|
||||
var vertex = end ? curve.End : curve.Start;
|
||||
if (point.DistanceTo(vertex) <= PostVerificationGeometry.Epsilon
|
||||
|| point.DistanceTo(vertex) <= JointTolerance
|
||||
&& (vertex.DistanceTo(other.Start) <= PostVerificationGeometry.Epsilon
|
||||
|| vertex.DistanceTo(other.End) <= PostVerificationGeometry.Epsilon))
|
||||
return end;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool Touch(CutGroup a, CutGroup b, CancellationToken token)
|
||||
{
|
||||
foreach (var curve in a.Curves.Concat(a.Leads))
|
||||
foreach (var other in b.Curves.Concat(b.Leads))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!curve.Extent.IsClearOf(other.Extent) && Contacts(curve, other).Count > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One contour's cut motions with the leads entering and leaving it.</summary>
|
||||
private sealed class CutGroup
|
||||
{
|
||||
internal List<PostVerificationGeometry.Curve> Curves { get; } = new();
|
||||
internal List<PostVerificationGeometry.Curve> Leads { get; } = new();
|
||||
internal bool Fixed { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DisjointSet(int count)
|
||||
{
|
||||
private readonly int[] parents = Enumerable.Range(0, count).ToArray();
|
||||
|
||||
internal int Find(int node)
|
||||
{
|
||||
while (parents[node] != node)
|
||||
node = parents[node] = parents[parents[node]];
|
||||
return node;
|
||||
}
|
||||
|
||||
internal void Union(int a, int b) => parents[Find(a)] = Find(b);
|
||||
}
|
||||
|
||||
private sealed record Obstacle(int Part, int Contour, IReadOnlyList<PostVerificationGeometry.Curve> Curves)
|
||||
{
|
||||
internal PostVerificationGeometry.Extent Extent { get; } =
|
||||
|
||||
@@ -139,7 +139,8 @@ public static class PostVerificationAnalyzer
|
||||
&& move.Layer is LayerType.Cut or LayerType.Display
|
||||
&& move.Curve.Length > PostVerificationGeometry.Epsilon))
|
||||
Incomplete("Placed program has no cutting contour motions for this drawing.");
|
||||
obstacles.AnalyzeMoves(moves, cutoff, findings, plateNumber, partNumber, cancellationToken);
|
||||
obstacles.AnalyzeMoves(moves, cutoff, findings, plateNumber, partNumber, cancellationToken,
|
||||
proveOpenCuts: true);
|
||||
if (clamps is { Count: > 0 })
|
||||
foreach (var move in moves)
|
||||
{
|
||||
|
||||
@@ -205,6 +205,25 @@ internal static class PostVerificationGeometry
|
||||
: Travel(System.Math.Atan2(point.Y - center.Y, point.X - center.X)) * Radius
|
||||
: Dot(point - Start, (End - Start) * (1 / Length));
|
||||
|
||||
/// <summary>Unit direction of travel at the curve's start or end.</summary>
|
||||
internal Vector Tangent(bool atEnd)
|
||||
{
|
||||
if (Center is not { } center)
|
||||
return (End - Start) * (1 / Length);
|
||||
var radial = (atEnd ? End : Start) - center;
|
||||
var turn = System.Math.Sign(Sweep);
|
||||
return new Vector(-radial.Y * turn, radial.X * turn) * (1 / Radius);
|
||||
}
|
||||
|
||||
/// <summary>How far the curve reaches from <paramref name="origin"/> along unit <paramref name="direction"/>.</summary>
|
||||
internal double Reach(Vector origin, Vector direction)
|
||||
{
|
||||
var reach = System.Math.Max(Dot(Start - origin, direction), Dot(End - origin, direction));
|
||||
if (Center is { } center && OnArc(center + direction * Radius))
|
||||
reach = System.Math.Max(reach, Dot(center - origin, direction) + Radius);
|
||||
return reach;
|
||||
}
|
||||
|
||||
internal Vector PointAtLength(double distance)
|
||||
{
|
||||
if (distance <= 0) return Start;
|
||||
|
||||
@@ -426,6 +426,42 @@ public class PostVerificationAnalyzerTests
|
||||
|| Find(report, PostVerificationKind.Incomplete).Any());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Rapid_CutoutCutInPiecesAroundTabsIsRetained(bool leads)
|
||||
{
|
||||
var report = Analyze(TabbedCutout(leads), Scribe(-2, 5), Scribe(12, 5));
|
||||
Assert.Empty(Find(report, PostVerificationKind.Incomplete));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rapid_LeadOutBridgingATabGapIsIncomplete()
|
||||
{
|
||||
var part = TabbedCutout(true);
|
||||
var codes = part.Program.Codes;
|
||||
// The second piece's lead-out runs across its tab to the first piece's start.
|
||||
codes[codes.FindLastIndex(code => code is LinearMove { Layer: LayerType.Leadout })] = new LinearMove(3, 5.1) { Layer = LayerType.Leadout };
|
||||
var report = Analyze(part, Scribe(-2, 5), Scribe(12, 5));
|
||||
Assert.NotEmpty(Find(report, PostVerificationKind.Incomplete));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rapid_OpenCutsAcrossAClosedContourAreIncomplete()
|
||||
{
|
||||
var part = Rectangle(0, 0, 10);
|
||||
var placed = Square(0, 0, 10);
|
||||
// Two cuts from edge to edge release the strip between them.
|
||||
placed.MoveTo(0, 3);
|
||||
placed.LineTo(10, 3);
|
||||
placed.MoveTo(10, 6);
|
||||
placed.LineTo(0, 6);
|
||||
part.RestoreLeadInProgram(placed, false);
|
||||
var report = Analyze(part, Scribe(-2, 5), Scribe(12, 5));
|
||||
Assert.Contains(Find(report, PostVerificationKind.Incomplete),
|
||||
finding => finding.Message.Contains("Multiple interrupted/open"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancellation_ThrowsWithoutChangingInputs()
|
||||
{
|
||||
@@ -506,6 +542,42 @@ public class PostVerificationAnalyzerTests
|
||||
return program;
|
||||
}
|
||||
|
||||
// A 10x10 part whose 4x4 cutout is cut in two pieces, leaving 0.2 tabs on its left and
|
||||
// right edges; leads enter and leave each piece from inside the slug.
|
||||
private static Part TabbedCutout(bool leads)
|
||||
{
|
||||
var clean = Square(3, 3, 4);
|
||||
clean.Codes.AddRange(Square(0, 0, 10).Codes);
|
||||
var part = new Part(new Drawing("tabbed", clean));
|
||||
var program = new Program();
|
||||
if (leads)
|
||||
{
|
||||
program.MoveTo(3.2, 5.3);
|
||||
program.Codes.Add(new LinearMove(3, 5.1) { Layer = LayerType.Leadin });
|
||||
}
|
||||
else
|
||||
program.MoveTo(3, 5.1);
|
||||
program.LineTo(3, 7);
|
||||
program.LineTo(7, 7);
|
||||
program.LineTo(7, 5.1);
|
||||
if (leads)
|
||||
{
|
||||
program.Codes.Add(new LinearMove(6.8, 5.3) { Layer = LayerType.Leadout });
|
||||
program.MoveTo(6.8, 4.7);
|
||||
program.Codes.Add(new LinearMove(7, 4.9) { Layer = LayerType.Leadin });
|
||||
}
|
||||
else
|
||||
program.MoveTo(7, 4.9);
|
||||
program.LineTo(7, 3);
|
||||
program.LineTo(3, 3);
|
||||
program.LineTo(3, 4.9);
|
||||
if (leads)
|
||||
program.Codes.Add(new LinearMove(3.2, 4.7) { Layer = LayerType.Leadout });
|
||||
program.Codes.AddRange(Square(0, 0, 10).Codes);
|
||||
part.RestoreLeadInProgram(program, false);
|
||||
return part;
|
||||
}
|
||||
|
||||
private static Part Scribe(double x, double y)
|
||||
{
|
||||
var program = new Program();
|
||||
|
||||
@@ -112,9 +112,17 @@ scrollable report, cancel to fix the nest, then post again to rerun verification
|
||||
Uncheckable geometry is reported as incomplete, not passed. The overlap check reads
|
||||
clean drawing programs made of the built-in instructions only; a missing instruction
|
||||
list, any other instruction type, or a hole stored as an absolute-mode subprogram
|
||||
(OpenNest writes them incremental) makes that part's check incomplete. Multiple interrupted
|
||||
cut fragments, or a lead-out after an open contour that might cut through a tab,
|
||||
also require manual review rather than being assumed retained.
|
||||
(OpenNest writes them incremental) makes that part's check incomplete.
|
||||
|
||||
Open cuts, such as a cutout cut in several pieces around tabs (with or without leads, as
|
||||
PEP programs often are), pass only when they provably release nothing. Within the part,
|
||||
each open piece and its leads may touch another cut only at their own ends, and together
|
||||
they may form no loop, counting each closed contour with its leads as one point. A lead at
|
||||
an open end must also stay out of the gap beyond it: a lead-out never reaches ahead of the
|
||||
end it leaves, and a lead-in never behind the start it enters. Anything else (pieces that
|
||||
meet exactly, a lead that bridges or curls into a tab, cuts from edge to edge) is reported
|
||||
as incomplete, needing manual review. Kerf, tab strength and other parts' cuts are not modelled. Plan
|
||||
Cutting keeps its stricter rule and does not certify its own tabs followed by a lead-out.
|
||||
|
||||
## Explicit override
|
||||
|
||||
|
||||
Reference in new issue
Block a user