fix(planning): cut the remnant cut-offs last

The last strip line stops at the remnant's full-length horizontal
separator, so it never counted as separating the sheet and the planner
cut it wherever the route passed it, often first (N26-264). That frees
the X-tail remnant early and loses the mass that absorbs heat while the
parts are cut.

A continuous line from a sheet edge or separator to another now counts
as a separator too (CutOff.Separators, found in rounds), so Plan Cutting
and Part Sequencing cut it after every part, just before the separator
it stops on. The problem-nest harness now adds scrap cut-offs with
KeepHorizontalRemnant, as both desktop entry points do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-03 12:50:33 -05:00
1 parent 5de628760f
commit 6c4ebe49b3
10 files changed
+212 -40

No files matched your search

+70 -9
View File
@@ -255,19 +255,80 @@ namespace OpenNest
/// splits the sheet in two. Anything else (gaps around parts, limits short of an edge, a
/// line on the sheet edge, a stale or unrecognized program) does not separate the sheet.
/// </summary>
internal static bool SeparatesSheet(Part part, CutOff definition, Box sheet)
internal static bool SeparatesSheet(Part part, CutOff definition, Box sheet) =>
ContinuousCut(part, definition, sheet) is { } cut
&& cut.Low <= (definition.Axis == CutOffAxis.Vertical ? sheet.Bottom : sheet.Left) + Math.Tolerance.Epsilon
&& cut.High >= (definition.Axis == CutOffAxis.Vertical ? sheet.Top : sheet.Right) - Math.Tolerance.Epsilon;
/// <summary>
/// The cut-off parts among <paramref name="parts"/> that split off a piece of the sheet,
/// each with the round it was found in. Round 0 holds the lines that
/// <see cref="SeparatesSheet"/> across the whole sheet; a line of a later round is one
/// continuous cut from a sheet edge or an earlier round's line to another, such as the
/// last strip line stopping at a remnant's horizontal separator. It frees a piece together
/// with the lines it stops on, so it is cut after every other part and before those lines
/// (higher rounds first), keeping the remnant's mass on the sheet until the end.
/// </summary>
public static IReadOnlyDictionary<Part, int> Separators(IEnumerable<Part> parts, IEnumerable<CutOff> definitions, Box sheet)
{
var rounds = new Dictionary<Part, int>(ReferenceEqualityComparer.Instance);
if (parts == null || definitions == null || sheet == null)
return rounds;
var byDrawing = new Dictionary<Drawing, CutOff>(ReferenceEqualityComparer.Instance);
foreach (var definition in definitions)
if (definition?.Drawing != null)
byDrawing[definition.Drawing] = definition;
var cuts = new List<(Part Part, bool Vertical, double Line, double Low, double High)>();
foreach (var part in parts)
if (part?.BaseDrawing is { IsCutOff: true } drawing && byDrawing.TryGetValue(drawing, out var definition)
&& ContinuousCut(part, definition, sheet) is { } cut)
{
var vertical = definition.Axis == CutOffAxis.Vertical;
cuts.Add((part, vertical, vertical ? definition.Position.X : definition.Position.Y, cut.Low, cut.High));
}
for (var round = 0; ; round++)
{
var found = cuts.Where(cut => !rounds.ContainsKey(cut.Part) && Bounded(cut)).ToList();
if (found.Count == 0)
return rounds;
foreach (var cut in found)
rounds[cut.Part] = round;
}
// The cut runs between two boundaries: sheet edges, or lines found in an earlier round
// that cross this one where they are cut. The stretch between them is cut, as the cut
// is continuous.
bool Bounded((Part Part, bool Vertical, double Line, double Low, double High) cut)
{
var reached = cuts.Where(other => other.Vertical != cut.Vertical && rounds.ContainsKey(other.Part)
&& other.Low <= cut.Line + Math.Tolerance.Epsilon && other.High >= cut.Line - Math.Tolerance.Epsilon)
.Select(other => other.Line)
.Append(cut.Vertical ? sheet.Bottom : sheet.Left)
.Append(cut.Vertical ? sheet.Top : sheet.Right)
.Where(b => b >= cut.Low - Math.Tolerance.Epsilon && b <= cut.High + Math.Tolerance.Epsilon)
.ToList();
return reached.Count > 1 && reached.Max() - reached.Min() > Math.Tolerance.Epsilon;
}
}
/// <summary>The cut-off parts on <paramref name="plate"/> that split off a piece of it, by round.</summary>
public static IReadOnlyDictionary<Part, int> Separators(Plate plate) =>
Separators(plate.Parts, plate.CutOffs, plate.BoundingBox(includeParts: false));
// The extent along the line of a part that cuts one continuous straight line on its
// definition's line strictly inside the sheet, or null for anything else.
private static (double Low, double High)? ContinuousCut(Part part, CutOff definition, Box sheet)
{
if (part?.Program == null || definition == null || sheet == null
|| part.Program.Mode != Mode.Absolute)
return false;
return null;
var vertical = definition.Axis == CutOffAxis.Vertical;
var line = vertical ? definition.Position.X : definition.Position.Y;
var acrossMin = vertical ? sheet.Left : sheet.Bottom;
var acrossMax = vertical ? sheet.Right : sheet.Top;
var alongMin = vertical ? sheet.Bottom : sheet.Left;
var alongMax = vertical ? sheet.Top : sheet.Right;
if (!(line > acrossMin + Math.Tolerance.Epsilon && line < acrossMax - Math.Tolerance.Epsilon))
return false;
return null;
Vector? position = null;
double? low = null, high = null;
@@ -285,13 +346,13 @@ namespace OpenNest
// A second pierce leaves an uncut gap; a head-locked rapid returns along
// the overcut to where the cut toward the origin begins.
if (cutting && !locked)
return false;
return null;
position = rapid.EndPoint + part.Location;
break;
case LinearMove move when position != null:
var end = move.EndPoint + part.Location;
if (!OnLine(position.Value) || !OnLine(end))
return false;
return null;
foreach (var value in new[] { Along(position.Value), Along(end) })
{
low = low == null ? value : System.Math.Min(low.Value, value);
@@ -301,10 +362,10 @@ namespace OpenNest
cutting = true;
break;
case Motion:
return false;
return null;
}
}
return cutting && low <= alongMin + Math.Tolerance.Epsilon && high >= alongMax - Math.Tolerance.Epsilon;
return cutting ? (low.Value, high.Value) : null;
bool OnLine(Vector point) =>
System.Math.Abs((vertical ? point.X : point.Y) - line) <= Math.Tolerance.Epsilon;
@@ -86,10 +86,12 @@ internal sealed class CuttingDependencyGraph
if (nodes[cut].SeparatesSheet)
{
// Cutting a line that splits the sheet comes after every part and every other
// cutoff, so the head never travels back across the separated edge. Such a line
// is continuous, so it passes through no part and needs no crossing edge.
// cutoff, so the head never travels back across the separated edge and the
// remnant's mass stays on to the end. Among separators, a line stopping on others
// (a later round) goes before them. Such a line is continuous, so it passes
// through no part and needs no crossing edge.
for (var other = 0; other < nodes.Count; other++)
if (!nodes[other].SeparatesSheet)
if (!nodes[other].SeparatesSheet || nodes[other].SeparatorRound > nodes[cut].SeparatorRound)
edges[cut].Add(other);
continue;
}
@@ -208,11 +210,11 @@ internal sealed class DependencyNode
private LeadMaterialSnapshot material;
internal DependencyNode(bool isCutOff, CutOffDefinition cutOff, Box placedBounds, Box cleanBounds,
Func<LeadMaterialSnapshot> materialFactory, bool separatesSheet = false, bool cutFirst = false)
Func<LeadMaterialSnapshot> materialFactory, int? separatorRound = null, bool cutFirst = false)
{
IsCutOff = isCutOff;
CutFirst = !isCutOff && cutFirst;
SeparatesSheet = isCutOff && separatesSheet;
SeparatorRound = isCutOff ? separatorRound : null;
CutOff = cutOff;
PlacedBounds = Copy(placedBounds);
CleanBounds = cleanBounds == null ? null : Copy(cleanBounds);
@@ -223,8 +225,13 @@ internal sealed class DependencyNode
}
internal bool IsCutOff { get; }
/// <summary>A cutoff whose placed program is one continuous cut across the whole sheet.</summary>
internal bool SeparatesSheet { get; }
/// <summary>
/// A cutoff whose placed program is one continuous cut splitting off a piece of the sheet,
/// alone or with the separators it stops on (see CutOff.Separators).
/// </summary>
internal bool SeparatesSheet => SeparatorRound != null;
/// <summary>The separator's round: 0 across the whole sheet, higher for lines stopping on earlier rounds.</summary>
internal int? SeparatorRound { get; }
/// <summary>A part whose drawing asks to be cut before the plate's other parts.</summary>
internal bool CutFirst { get; }
internal CutOffDefinition CutOff { get; }
@@ -57,6 +57,8 @@ public static class CuttingPlanService
// Each cut-off part cuts one trimmed segment of its line and precedes only the parts
// in that segment's span (docs/automatic-scrap-cutoffs.md), not every part the line passes.
var spans = new Dictionary<CutOff, Dictionary<Part, (double Start, double End)>>(ReferenceEqualityComparer.Instance);
// Lines splitting off a piece of the sheet, alone or with the separators they stop on.
IReadOnlyDictionary<Part, int> separators = null;
var nodes = new List<DependencyNode>();
// Reuse the reader's coordinate validation without publishing its native kernel.
var identities = new HashSet<Part>(ReferenceEqualityComparer.Instance);
@@ -84,7 +86,9 @@ public static class CuttingPlanService
throw new ArgumentException("Placed cutoff program has no nonzero cutting motions.");
var defined = definitions.TryGetValue(source.BaseDrawing, out var definition);
var sheet = request.Plate.BoundingBox(includeParts: false);
var separates = defined && CutOff.SeparatesSheet(source, definition, sheet);
separators ??= CutOff.Separators(request.Parts, definitions.Values, sheet);
int? round = defined && separators.TryGetValue(source, out var found) ? found : null;
var separates = round != null;
CutOffDefinition nominal = null;
if (defined)
{
@@ -99,7 +103,7 @@ public static class CuttingPlanService
: new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit,
definition.Gaps);
}
nodes.Add(new(true, nominal, source.BoundingBox, null, null, separates));
nodes.Add(new(true, nominal, source.BoundingBox, null, null, round));
// A head-locked far-edge segment may be cut from either end (docs/automatic-scrap-cutoffs.md).
FixedProgramPlacement reversed = null;
if (defined && CutOff.ReverseAtFarEdge(source.Program) is { } flipped)
+19 -14
View File
@@ -40,8 +40,8 @@ namespace OpenNest.Engine.Sequencing
/// Moves the given cutoffs' segment parts to where the cutoff rules need them while every
/// other part and cutoff keeps its position: each segment just before the first part in
/// its span of the nominal line (see <see cref="CutOffSegments"/>), or last when it splits
/// the whole sheet. One that crosses nothing stays put, unless it would follow a
/// sheet-separating cutoff.
/// off a piece of the sheet (<see cref="CutOff.Separators(Plate)"/>), before any separator it
/// stops on. One that crosses nothing stays put, unless it would follow a separator.
/// </summary>
public static void PlaceCutOffs(Plate plate, IEnumerable<CutOff> definitions)
{
@@ -54,15 +54,13 @@ namespace OpenNest.Engine.Sequencing
return;
var movingParts = new HashSet<Part>(moving.Select(item => item.Segment.Part), ReferenceEqualityComparer.Instance);
var existing = new Dictionary<Drawing, CutOff>(ReferenceEqualityComparer.Instance);
foreach (var cutOff in plate.CutOffs)
existing[cutOff.Drawing] = cutOff;
var order = plate.Parts.ToList();
var separators = CutOff.Separators(plate);
var separating = new List<Part>();
foreach (var (definition, segment) in moving)
{
var part = segment.Part;
if (CutOff.SeparatesSheet(part, definition, bounds))
if (separators.ContainsKey(part))
{
order.Remove(part);
separating.Add(part);
@@ -72,14 +70,20 @@ namespace OpenNest.Engine.Sequencing
&& CuttingDependencyGraph.CutOffCrosses(definition.Axis, definition.Position,
segment.SpanStart, segment.SpanEnd, other.BoundingBox, bounds, definition.Gaps));
if (target < 0)
target = order.FindIndex(other => other.BaseDrawing.IsCutOff && !movingParts.Contains(other)
&& existing.TryGetValue(other.BaseDrawing, out var cut) && CutOff.SeparatesSheet(other, cut, bounds));
target = order.FindIndex(other => !movingParts.Contains(other) && separators.ContainsKey(other));
if (target < 0 || target > order.IndexOf(part))
continue;
order.Remove(part);
order.Insert(target, part);
}
order.AddRange(separating);
// Each moved separator goes last, but before a staying separator of an earlier round:
// one it may stop on.
foreach (var part in separating.OrderByDescending(part => separators[part]))
{
var before = order.FindIndex(other => !movingParts.Contains(other)
&& separators.TryGetValue(other, out var round) && round < separators[part]);
order.Insert(before < 0 ? order.Count : before, part);
}
// Touch only the moved cutoff parts; real parts never leave the plate.
foreach (var part in movingParts)
@@ -110,11 +114,12 @@ namespace OpenNest.Engine.Sequencing
foreach (var segment in CutOffSegments.Of(cutOff, ordered, bounds))
spans[segment.Part] = (segment.SpanStart, segment.SpanEnd);
// A cutoff that splits the whole sheet goes after every part and other cutoff, so the
// head never travels back across it. It passes through no part.
var separating = ordered.Where(part => part.BaseDrawing.IsCutOff
&& definitions.TryGetValue(part.BaseDrawing, out var definition)
&& CutOff.SeparatesSheet(part, definition, bounds)).ToList();
// A cutoff that splits off a piece of the sheet goes after every part and other cutoff,
// so the head never travels back across it and the remnant's mass stays on to the end;
// one stopping on other separators goes before them. It passes through no part.
var separators = CutOff.Separators(ordered, plate.CutOffs, bounds);
var separating = ordered.Where(separators.ContainsKey)
.OrderByDescending(part => separators[part]).ToList();
if (separating.Count > 0)
{
var last = new HashSet<Part>(separating, ReferenceEqualityComparer.Instance);
@@ -3,7 +3,7 @@ using OpenNest.Geometry;
namespace OpenNest.Tests.CutOffs;
/// <summary>Which cut-offs split the whole sheet (and so must be cut after every part).</summary>
/// <summary>Which cut-offs split off a piece of the sheet (and so must be cut after every part).</summary>
public class CutOffSeparationTests
{
[Theory]
@@ -57,6 +57,33 @@ public class CutOffSeparationTests
Assert.False(CutOff.SeparatesSheet(Part(plate, limited), null, plate.BoundingBox(false)));
}
// The automatic remnant layout: a full-length horizontal separator above the parts and strip
// lines stopping at it. The last line, clear of the parts, frees the corner remnant together
// with the separator, so it separates in the next round, as does a line from it to the far
// edge. A strip line trimmed around a part and a line short of the separator do not.
[Fact]
public void LinesStoppingOnSeparators_SeparateInLaterRounds()
{
var plate = new Plate(60, 120);
plate.Parts.Add(TestHelpers.MakePartAt(10, 10, 10));
var horizontal = new CutOff(new Vector(0, 30), CutOffAxis.Horizontal);
var remnant = new CutOff(new Vector(70, 0), CutOffAxis.Vertical) { EndLimit = 30 };
var beyond = new CutOff(new Vector(0, 15), CutOffAxis.Horizontal) { StartLimit = 70 };
var strip = new CutOff(new Vector(15, 0), CutOffAxis.Vertical) { EndLimit = 30 };
var shortOf = new CutOff(new Vector(50, 0), CutOffAxis.Vertical) { EndLimit = 20 };
plate.CutOffs.AddRange([horizontal, remnant, beyond, strip, shortOf]);
plate.RegenerateCutOffs(new CutOffSettings());
var separators = CutOff.Separators(plate);
Assert.Equal(0, separators[Part(plate, horizontal)]);
Assert.Equal(1, separators[Part(plate, remnant)]);
Assert.Equal(2, separators[Part(plate, beyond)]);
Assert.False(Separates(plate, remnant)); // Not across the whole sheet on its own.
Assert.Equal(3, separators.Count);
Assert.DoesNotContain(Part(plate, shortOf), separators.Keys);
}
// A line trimmed around a part is two segment parts, neither of which spans the sheet.
private static bool Separates(Plate plate, CutOff cutOff) =>
TestHelpers.CutOffParts(plate, cutOff) is [var part]
@@ -66,6 +66,39 @@ public class CuttingDependencyTests
Assert.Contains(right, plate.Parts);
}
// N26-264: the last strip line stops at the remnant's full-length horizontal separator. It
// frees the corner remnant with the separator, so both are cut after every part, keeping the
// remnant's mass on the sheet to absorb heat; the line goes first, as it stops on the separator.
[Fact]
public void Plan_LineStoppingOnTheRemnantSeparator_IsCutLastBeforeTheSeparator()
{
var nest = new Nest();
var plate = nest.CreatePlate();
plate.Size = new Size(100, 100);
var low = new Part(new Drawing("low", ExplicitContourTests.Square(false)), new Vector(1, 1));
var high = new Part(new Drawing("high", ExplicitContourTests.Square(false)), new Vector(1, 20));
plate.Parts.Add(low);
plate.Parts.Add(high);
var vertical = new CutOff(new Vector(40, 0), CutOffAxis.Vertical) { EndLimit = 40 };
var horizontal = new CutOff(new Vector(0, 40), CutOffAxis.Horizontal);
plate.CutOffs.Add(vertical);
plate.CutOffs.Add(horizontal);
plate.RegenerateCutOffs(new CutOffSettings());
var line = plate.Parts.Single(p => ReferenceEquals(p.BaseDrawing, vertical.Drawing));
var separator = plate.Parts.Single(p => ReferenceEquals(p.BaseDrawing, horizontal.Drawing));
plate.Parts.Reorder([line, low, separator, high]);
var snapshot = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
confirmedParameters: ExplicitContourTests.Parameters()));
Assert.Equal(new[] { 1, 3 }, snapshot.Dependencies.PrerequisitesOf(0));
Assert.Equal(new[] { 0, 1, 3 }, snapshot.Dependencies.PrerequisitesOf(2));
var result = CuttingPlanService.Plan(snapshot);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.Equal(new[] { line, separator }, result.ProposedOrder.Skip(2).Select(o => o.SourcePart));
Assert.All(result.ProposedOrder.Skip(2), o => Assert.True(o.SeparatesSheet));
}
[Fact]
public void Plan_KeptOrderCuttingTheSeparatorFirst_IsAConstraintConflictThatSaysWhy()
{
@@ -156,6 +156,7 @@ public partial class ProblemNestRegressionTests(ITestOutputHelper output)
Spacing = spacing,
MinimumStripLength = spacing / 2.0,
MinimumTailLength = units == Units.Millimeters ? 304.8 : 12,
KeepHorizontalRemnant = true, // As both desktop entry points set it.
}, new CutOffSettings());
Assert.False(staging.IsBlocked, $"Plate {number}'s scrap cut-offs are blocked.");
PlateSequencing.PlaceCutOffs(plate, staging.Plans[0].Definitions);
@@ -189,6 +189,35 @@ public class PlateSequencingTests
AssertPrecedes(plate, cutB, first, second);
}
// The remnant's lines go last: the strip line stopping on the separator, then the separator.
[Theory]
[InlineData(SequenceMethod.RightSide)]
[InlineData(SequenceMethod.LeftSide)]
[InlineData(SequenceMethod.LeastCode)]
public void Apply_RemnantLines_AreCutLastInnerFirst(SequenceMethod method)
{
var plate = new Plate(60, 120);
plate.Parts.Add(TestHelpers.MakePartAt(10, 10, 10));
plate.Parts.Add(TestHelpers.MakePartAt(50, 10, 10));
var vertical = new CutOff(new Vector(80, 0), CutOffAxis.Vertical) { EndLimit = 30 };
var horizontal = new CutOff(new Vector(0, 30), CutOffAxis.Horizontal);
plate.CutOffs.Add(horizontal);
plate.CutOffs.Add(vertical);
plate.RegenerateCutOffs(new CutOffSettings());
plate.Parts.Reorder([.. plate.Parts.Where(p => p.BaseDrawing.IsCutOff), .. plate.Parts.Where(p => !p.BaseDrawing.IsCutOff)]);
PlateSequencing.Apply(plate, new SequenceParameters { Method = method });
Assert.Same(vertical.Drawing, plate.Parts[^2].BaseDrawing);
Assert.Same(horizontal.Drawing, plate.Parts[^1].BaseDrawing);
// Placing them again after an edit keeps that order.
plate.Parts.Reorder([plate.Parts[^1], plate.Parts[^2], .. plate.Parts.Take(plate.Parts.Count - 2)]);
PlateSequencing.PlaceCutOffs(plate, [horizontal, vertical]);
Assert.Same(vertical.Drawing, plate.Parts[^2].BaseDrawing);
Assert.Same(horizontal.Drawing, plate.Parts[^1].BaseDrawing);
}
[Theory]
[InlineData(SequenceMethod.RightSide)]
[InlineData(SequenceMethod.LeftSide)]
+2 -2
View File
@@ -34,7 +34,7 @@ When the parts leave an unused band beyond them along Y (away from the origin),
- The desktop dialog and Plan Cutting's `Add scrap cut-offs` also propose a new full-length horizontal separator beyond the parts, at the same margin as the X tail separator, when the band is at least **Minimum tail to keep** wide and its area beats the X tail it competes with. The vertical lines then stop at it. API callers opt in with `AutomaticCutOffOptions.KeepHorizontalRemnant`; without it they get full-width lines unless a horizontal cut-off already exists.
- Beside a kept band, the X tail becomes a corner piece. Its separator stops at the band too, and it is kept only when both its length and the distance to the band are at least the minimum; otherwise the strip lines continue to the sheet end.
For a 96-by-48-inch sheet with parts through Y 23 and a horizontal cut-off at Y 23.57, the 35-inch strip lines (shifted as usual) cut from Y 0 to 23.57 and the 24.43-inch band stays whole. Rerunning recognizes the stopped lines as existing. A new separator is not proposed when it would cross an active clamp or a same-line manual cut with other limits, or when its program is not one uninterrupted cut; the details list reports `NoSafeHorizontalSeparator`, and the lines cross the full width. A `HorizontalRemnant` notice reports the separator's Y and the band width. The separator splits the sheet, so [Part sequencing](#part-sequencing) cuts it last.
For a 96-by-48-inch sheet with parts through Y 23 and a horizontal cut-off at Y 23.57, the 35-inch strip lines (shifted as usual) cut from Y 0 to 23.57 and the 24.43-inch band stays whole. Rerunning recognizes the stopped lines as existing. A new separator is not proposed when it would cross an active clamp or a same-line manual cut with other limits, or when its program is not one uninterrupted cut; the details list reports `NoSafeHorizontalSeparator`, and the lines cross the full width. A `HorizontalRemnant` notice reports the separator's Y and the band width. The separator splits the sheet, so [Part sequencing](#part-sequencing) cuts it last, and a strip line that reaches it without crossing a part (the one bounding the X tail) just before it, so both remnants keep their mass on the sheet until the parts are cut.
## Shifting strip lines
@@ -81,7 +81,7 @@ For **Plate > Sequence Parts > Manual Sequence**, choose the one-based sequence
For **Plate > Sequence Parts > Sequence by Path**, drag lines over the sheet with the left button in the order you want the parts cut; clicking a part adds it as a one-point line that claims the parts whose bounding boxes hold the click. Lifting the button between lines is a rapid: the dashed jump to the next line's numbered start claims no parts. Each part belongs to the line crossing its bounding box that passes closest to its bounding-box center, and lines are cut in drawing order, each in the order its nearest points fall along it; where a line passes a part more than once, the closest pass decides. Parts no line crosses are cut after all the others, in the order they would follow their nearest line, and are drawn muted until a line or click reaches them. The numbers update as you drag, and the plate is reordered when you release. **Undo Line**, Backspace or Ctrl+Z removes the last line; **Clear** or right-click removes them all and restores the order the tool started with. Cut-first parts stay ahead of the rest, and cutoffs follow the same dependency rules as Part Sequencing below. Escape mid-drag drops only that line; otherwise Escape, **Done** or closing the window leaves the tool.
After adding cutoffs, apply **Part Sequencing** to the current plate or all plates. Each trimmed segment of a cutoff is a part of its own in the sequence, and each is moved earlier as needed so it is cut before every part its nominal line passes through within the segment's span: the stretch of the line from the end of the segment before it to the start of the one after it (or the line's start or end). The parts a line passes through lie in the gaps between its segments, so each is cut after the segment on either side of it and no other; the segment below a row of parts is cut with that row, not before a row the line passes through farther on. Segments due before the same part keep the order they were regenerated in. Ordinary parts retain their relative order from the chosen sequencing route. A cutoff that splits the whole sheet — one continuous cut from sheet edge to sheet edge, such as a tail separator — goes after every part and every other cutoff, so the head never travels back across the separated edge. Cutoffs that cross no part but leave gaps keep their normal route position. Cutoff geometry, clearance, part programs, placements, and quantities are unchanged.
After adding cutoffs, apply **Part Sequencing** to the current plate or all plates. Each trimmed segment of a cutoff is a part of its own in the sequence, and each is moved earlier as needed so it is cut before every part its nominal line passes through within the segment's span: the stretch of the line from the end of the segment before it to the start of the one after it (or the line's start or end). The parts a line passes through lie in the gaps between its segments, so each is cut after the segment on either side of it and no other; the segment below a row of parts is cut with that row, not before a row the line passes through farther on. Segments due before the same part keep the order they were regenerated in. Ordinary parts retain their relative order from the chosen sequencing route. A cutoff that splits the whole sheet — one continuous cut from sheet edge to sheet edge, such as a tail separator — goes after every part and every other cutoff, so the head never travels back across the separated edge. So does one continuous cut from a sheet edge or such a separator to another, such as the last strip line stopping at a horizontal remnant separator: it frees a remnant with the lines it stops on, and goes just before them, so the remnants' mass stays on the sheet to absorb heat until the parts are cut. Cutoffs that cross no part but leave gaps keep their normal route position. Cutoff geometry, clearance, part programs, placements, and quantities are unchanged.
The dependency check uses the nominal horizontal/vertical line within the segment's span, not the trimmed cutting segment itself, which intentionally skips the parts (`CutOffSegments` measures the spans from the segment parts). It conservatively checks placed part bounds (including edge contacts), so a cutoff through a concave recess can also move ahead of that part. Definitions are matched by drawing identity, not their displayed names. A cutoff part with no matching definition is conservatively ordered before all ordinary parts, and a segment part whose cut cannot be measured stands for the whole line.
+10 -5
View File
@@ -136,11 +136,16 @@ the final replay enforce them:
- A cutoff that splits the whole sheet is cut after every part and every other
cutoff, so the head never travels back across the separated edge. It qualifies
when its placed program is one continuous straight cut on its own line spanning
the sheet from edge to edge (`CutOff.SeparatesSheet`). A line trimmed around parts,
limited short of an edge, on the sheet edge, stale after a move or in an
unrecognized program form does not qualify and keeps the crossing rule above.
Being continuous, a separating line passes through no part. A kept order that cuts
it earlier is a constraint conflict naming the separating cutoff.
the sheet from edge to edge (`CutOff.SeparatesSheet`). A continuous line from a
sheet edge or separator to another, such as the last strip line stopping at a
remnant's horizontal separator, also qualifies (`CutOff.Separators`): it frees a
piece with the lines it stops on, so the remnants are cut last and their mass stays
on the sheet to absorb heat. It is cut just before the separators it stops on. A
line trimmed around parts, limited short of an edge or separator, on the sheet edge,
stale after a move or in an unrecognized program form does not qualify and keeps
the crossing rule above. Being continuous, a separating line passes through no
part. A kept order that cuts it earlier is a constraint conflict naming the
separating cutoff.
- A drawing's break cuts (`LayerType.Break`, see [breakoffs](breakoffs.md)) are open
cuts inside its material. Preparation keeps them apart from contours (no choices,
no material); every full and prefix emission cuts them after scribes and before any