mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 01:31:25 -04:00
feat(cutting): plan each scrap cut-off segment where the route passes it
Plates with staged automatic scrap cut-offs could not be planned: P260810-01 plate 1 ran out of budget even at 10x effort, while the same plate plans Ready in 3.6k expansions without the lines. A cut-off was one fixed program cut whole before every part its line passes through, which dragged the tour to the middle of the sheet and left the head at the far edge of the line; and cutting every line first (the rejected alternative) weakens the sheet and breaks the containment operators rely on when they fast-forward a changed program. A cut-off stays one definition, but Plate.RegenerateCutOffs now makes one part per trimmed segment, each with its own place in the cut sequence. CutOffSegments gives each segment the span of the nominal line it stands for (end of the segment before it to start of the one after), and the dependency rules of the planner and Part Sequencing use that span, so a segment precedes only the parts beside it. The free-order search plans the parts as if the segments were not there, so adding lines no longer changes a plate's route, then CutOffInsertion slides each segment into the latest gap before its first part where the rapid into its pierce and the rapid from its end cross nothing cut before; only when one fits nowhere is the plate searched with the segments among the parts. The search looks through segments wherever they sit (entries face the next part, hole routes rank from the pre-segment position, rapids are still checked from the segment's end), so an applied plan replanned with the order kept replays Ready. Segments saved out of consecutive order are recorded per segment in the .nest file (`segments`); older readers load them consecutively. P260810-01 plate 1 now plans Ready in 3639 expansions with every check passing; rapid travel 1836 against 1547 without the lines. Plates without cut-offs keep their PlanningCharacterizationTests digests, and the problem-nest corpus has no plate below its baseline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff46de69df
commit
8f27eac2ca
30 files changed
+1149
-187
No files matched your search
@@ -39,6 +39,29 @@ namespace OpenNest
|
||||
Drawing.Program = program;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The regenerated program split into its cutting segments, one rapid-and-cut program
|
||||
/// each in program order, so the plate can sequence every segment on its own.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Program> SegmentPrograms()
|
||||
{
|
||||
var program = Drawing.Program;
|
||||
var result = new List<Program>();
|
||||
if (program == null)
|
||||
return result;
|
||||
Program current = null;
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
if (code is RapidMove || current == null)
|
||||
{
|
||||
current = new Program(program.Mode);
|
||||
result.Add(current);
|
||||
}
|
||||
current.Codes.Add(code);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Measures the same trimmed, filtered segments as regeneration without building a program.</summary>
|
||||
internal (double Length, int SegmentCount) MeasureSegments(
|
||||
Plate plate,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// One placed cutting segment of a cut-off: the part that cuts it, where its cut runs along
|
||||
/// the cut-off's axis, and the span of the nominal line it stands for, from the end of the
|
||||
/// segment before it (or the line's start) to the start of the one after it (or the line's
|
||||
/// end). The parts the nominal line passes through lie in the gaps between segments, so each
|
||||
/// is inside the span of the segment on either side of it; a segment is cut before exactly
|
||||
/// those parts.
|
||||
/// </summary>
|
||||
public sealed record CutOffSegment(Part Part, double Start, double End, double SpanStart, double SpanEnd);
|
||||
|
||||
/// <summary>
|
||||
/// A cut-off's trimmed segments are materialized as one part each, so every segment has its
|
||||
/// own place in the cut sequence. This finds those parts and the span each stands for.
|
||||
/// </summary>
|
||||
public static class CutOffSegments
|
||||
{
|
||||
/// <summary>The definition's placed segment parts in ascending order along its axis.</summary>
|
||||
public static IReadOnlyList<CutOffSegment> Of(Plate plate, CutOff definition) =>
|
||||
Of(definition, plate.Parts, plate.BoundingBox(includeParts: false));
|
||||
|
||||
/// <summary>
|
||||
/// The definition's segment parts among <paramref name="parts"/> (those placed from its
|
||||
/// drawing) in ascending order along its axis, with spans measured against the nominal
|
||||
/// line's limits on <paramref name="sheet"/>. A part whose cut cannot be measured stands
|
||||
/// for the whole line.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CutOffSegment> Of(CutOff definition, IEnumerable<Part> parts, Box sheet)
|
||||
{
|
||||
var vertical = definition.Axis == CutOffAxis.Vertical;
|
||||
var lineStart = definition.StartLimit ?? (vertical ? sheet.Bottom : sheet.Left);
|
||||
var lineEnd = definition.EndLimit ?? (vertical ? sheet.Top : sheet.Right);
|
||||
var low = System.Math.Min(lineStart, lineEnd);
|
||||
var high = System.Math.Max(lineStart, lineEnd);
|
||||
var measured = new List<(Part Part, double Start, double End)>();
|
||||
var whole = new List<CutOffSegment>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (!ReferenceEquals(part.BaseDrawing, definition.Drawing))
|
||||
continue;
|
||||
if (Extent(part, vertical) is { } extent)
|
||||
measured.Add((part, extent.Start, extent.End));
|
||||
else
|
||||
whole.Add(new CutOffSegment(part, low, high, low, high));
|
||||
}
|
||||
measured.Sort((a, b) => a.Start != b.Start ? a.Start.CompareTo(b.Start) : a.End.CompareTo(b.End));
|
||||
var result = new List<CutOffSegment>(measured.Count + whole.Count);
|
||||
for (var i = 0; i < measured.Count; i++)
|
||||
{
|
||||
var (part, start, end) = measured[i];
|
||||
var spanStart = i == 0 ? System.Math.Min(low, start) : measured[i - 1].End;
|
||||
var spanEnd = i == measured.Count - 1 ? System.Math.Max(high, end) : measured[i + 1].Start;
|
||||
result.Add(new CutOffSegment(part, start, end, spanStart, spanEnd));
|
||||
}
|
||||
result.AddRange(whole);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The extent along the cut-off's axis of the part's cutting moves, or null when the
|
||||
/// part cuts nothing or its program is not the absolute rapid-and-cut form cut-offs use.
|
||||
/// </summary>
|
||||
public static (double Start, double End)? Extent(Part part, bool vertical)
|
||||
{
|
||||
if (part?.Program == null || part.Program.Mode != Mode.Absolute)
|
||||
return null;
|
||||
double? min = null, max = null;
|
||||
Vector? position = null;
|
||||
foreach (var code in part.Program.Codes)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case RapidMove rapid:
|
||||
position = rapid.EndPoint + part.Location;
|
||||
break;
|
||||
case Motion motion when position != null:
|
||||
var end = motion.EndPoint + part.Location;
|
||||
foreach (var point in new[] { position.Value, end })
|
||||
{
|
||||
var along = vertical ? point.Y : point.X;
|
||||
min = min == null ? along : System.Math.Min(min.Value, along);
|
||||
max = max == null ? along : System.Math.Max(max.Value, along);
|
||||
}
|
||||
position = end;
|
||||
break;
|
||||
case Motion:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return min == null ? null : (min.Value, max.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,15 @@ namespace OpenNest
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
/// <summary>A placement of one owned program of the drawing, such as one cut-off segment.</summary>
|
||||
internal Part(Drawing baseDrawing, Program program)
|
||||
{
|
||||
BaseDrawing = baseDrawing;
|
||||
Program = program;
|
||||
ownsProgram = true;
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Location of the part.
|
||||
/// </summary>
|
||||
|
||||
+90
-27
@@ -266,17 +266,16 @@ namespace OpenNest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates all cut-off drawings and materializes them as parts.
|
||||
/// Existing cut-off parts are removed first, then each cut-off is
|
||||
/// regenerated and put back at the same place in the cut sequence
|
||||
/// (<see cref="Parts"/> order). New cut-offs are added at the end.
|
||||
/// Regenerates all cut-off drawings and materializes each of their trimmed segments as
|
||||
/// a part. Existing cut-off parts are removed first, then each regenerated segment goes
|
||||
/// back to the place in the cut sequence (<see cref="Parts"/> order) of the saved segment
|
||||
/// of its cut-off it overlaps most, so a part drag or cut-off move doesn't send it to the
|
||||
/// end of the sequence; a segment with no saved counterpart follows the segment before
|
||||
/// it. New cut-offs are added at the end.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings)
|
||||
{
|
||||
// Remember each cut-off's place in the cut sequence, so a part drag
|
||||
// or cut-off move doesn't send it to the end of the sequence.
|
||||
var sequence = new Dictionary<CutOff, int>();
|
||||
|
||||
var saved = new Dictionary<CutOff, List<(int Index, double Start, double End)>>();
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!Parts[i].BaseDrawing.IsCutOff)
|
||||
@@ -284,44 +283,108 @@ namespace OpenNest
|
||||
|
||||
var cutoff = CutOffs.FirstOrDefault(c => ReferenceEquals(c.Drawing, Parts[i].BaseDrawing));
|
||||
if (cutoff != null)
|
||||
sequence[cutoff] = i;
|
||||
{
|
||||
if (!saved.TryGetValue(cutoff, out var segments))
|
||||
saved[cutoff] = segments = new List<(int, double, double)>();
|
||||
var extent = CutOffSegments.Extent(Parts[i], cutoff.Axis == CutOffAxis.Vertical);
|
||||
segments.Add((i, extent?.Start ?? double.NaN, extent?.End ?? double.NaN));
|
||||
}
|
||||
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
RegenerateCutOffs(settings, sequence);
|
||||
RegenerateCutOffs(settings, (cutoff, extents) =>
|
||||
{
|
||||
if (!saved.TryGetValue(cutoff, out var segments))
|
||||
return null;
|
||||
var indices = new int[extents.Count];
|
||||
var previous = segments.Min(s => s.Index);
|
||||
for (var k = 0; k < extents.Count; k++)
|
||||
{
|
||||
var (start, end) = extents[k];
|
||||
var best = -1;
|
||||
var bestOverlap = 0.0;
|
||||
foreach (var (index, savedStart, savedEnd) in segments)
|
||||
{
|
||||
var overlap = System.Math.Min(end, savedEnd) - System.Math.Max(start, savedStart);
|
||||
if (overlap > bestOverlap)
|
||||
{
|
||||
best = index;
|
||||
bestOverlap = overlap;
|
||||
}
|
||||
}
|
||||
indices[k] = previous = best >= 0 ? best : previous;
|
||||
}
|
||||
return indices;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates all cut-off drawings and materializes them as parts, placing
|
||||
/// each cut-off at its index in <paramref name="sequence"/> (its place in
|
||||
/// <see cref="Parts"/> order). Cut-offs missing from it are added at the end.
|
||||
/// Callers must remove existing cut-off parts first.
|
||||
/// Regenerates all cut-off drawings and materializes each of their trimmed segments as a
|
||||
/// part, placing a cut-off's segments consecutively from its index in
|
||||
/// <paramref name="sequence"/> (its place in <see cref="Parts"/> order). Cut-offs missing
|
||||
/// from it are added at the end. Callers must remove existing cut-off parts first.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary<CutOff, int> sequence)
|
||||
public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary<CutOff, int> sequence) =>
|
||||
RegenerateCutOffs(settings, (cutoff, extents) =>
|
||||
sequence != null && sequence.TryGetValue(cutoff, out var index)
|
||||
? Enumerable.Range(index, extents.Count).ToArray() : null);
|
||||
|
||||
/// <summary>
|
||||
/// As <see cref="RegenerateCutOffs(CutOffSettings, IReadOnlyDictionary{CutOff, int})"/>,
|
||||
/// but each cut-off lists one index per segment in ascending order along its axis. A list
|
||||
/// whose length differs from the regenerated segment count places the segments
|
||||
/// consecutively from its smallest index.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary<CutOff, IReadOnlyList<int>> sequence) =>
|
||||
RegenerateCutOffs(settings, (cutoff, extents) =>
|
||||
{
|
||||
if (sequence == null || !sequence.TryGetValue(cutoff, out var indices) || indices == null || indices.Count == 0)
|
||||
return null;
|
||||
if (indices.Count != extents.Count)
|
||||
return Enumerable.Range(indices.Min(), extents.Count).ToArray();
|
||||
// Program order can run toward the origin; the file lists segments ascending.
|
||||
var ascending = Enumerable.Range(0, extents.Count).OrderBy(k => extents[k].Start).ToArray();
|
||||
var result = new int[extents.Count];
|
||||
for (var rank = 0; rank < ascending.Length; rank++)
|
||||
result[ascending[rank]] = indices[rank];
|
||||
return result;
|
||||
});
|
||||
|
||||
// sequence gives a cut-off's segment indices in program order from its segments' extents
|
||||
// along its axis (NaN when unmeasurable), or null to append them.
|
||||
private void RegenerateCutOffs(CutOffSettings settings,
|
||||
Func<CutOff, IReadOnlyList<(double Start, double End)>, IReadOnlyList<int>> sequence)
|
||||
{
|
||||
var cache = BuildPerimeterCache(this);
|
||||
var placed = new List<(int Index, Part Part)>();
|
||||
var placed = new List<(int Index, int Order, Part Part)>();
|
||||
|
||||
// Regenerate and materialize each cut-off
|
||||
// Regenerate and materialize each cut-off's segments
|
||||
foreach (var cutoff in CutOffs)
|
||||
{
|
||||
cutoff.Regenerate(this, settings, cache);
|
||||
|
||||
if (cutoff.Drawing.Program.Codes.Count == 0)
|
||||
var programs = cutoff.SegmentPrograms();
|
||||
if (programs.Count == 0)
|
||||
continue;
|
||||
|
||||
var part = new Part(cutoff.Drawing);
|
||||
|
||||
if (sequence != null && sequence.TryGetValue(cutoff, out var index))
|
||||
placed.Add((index, part));
|
||||
else
|
||||
Parts.Add(part);
|
||||
var vertical = cutoff.Axis == CutOffAxis.Vertical;
|
||||
var parts = programs.Select(program => new Part(cutoff.Drawing, program)).ToArray();
|
||||
var extents = parts.Select(part => CutOffSegments.Extent(part, vertical) ?? (double.NaN, double.NaN)).ToArray();
|
||||
var indices = sequence?.Invoke(cutoff, extents);
|
||||
for (var k = 0; k < parts.Length; k++)
|
||||
{
|
||||
if (indices != null && k < indices.Count)
|
||||
placed.Add((indices[k], placed.Count, parts[k]));
|
||||
else
|
||||
Parts.Add(parts[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Lowest index first: each insert then lands on its saved index, because
|
||||
// every part sequenced before it is already in place.
|
||||
foreach (var (index, part) in placed.OrderBy(p => p.Index))
|
||||
// Lowest index first: each insert then lands on its saved index, because every part
|
||||
// sequenced before it is already in place. Segments sharing an index keep their
|
||||
// program order (a later insert at the same index lands before an earlier one).
|
||||
foreach (var (index, _, part) in placed.OrderBy(p => p.Index).ThenByDescending(p => p.Order))
|
||||
Parts.Insert(System.Math.Clamp(index, 0, Parts.Count), part);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Places cut-off segments into a plan made without them. A segment is a fixed open cut that
|
||||
/// is cut the same way wherever it goes and is never a rapid obstacle, so all that changes with
|
||||
/// its place in the order are the rapid into its pierce and the rapid from its end to what
|
||||
/// follows. Each segment goes as late as it can before the first part in its span of the line,
|
||||
/// where both rapids cross nothing cut before that point; the part after it keeps the program
|
||||
/// it was planned with. Segments due in the same gap are chained in the order placed.
|
||||
/// </summary>
|
||||
internal static class CutOffInsertion
|
||||
{
|
||||
/// <summary>
|
||||
/// The non-separating cut-offs that must precede some part, by source ordinal. A separator
|
||||
/// or a cut-off through open scrap precedes nothing and is planned like a part.
|
||||
/// </summary>
|
||||
internal static int[] Attached(CuttingPlanSnapshot snapshot)
|
||||
{
|
||||
var count = snapshot.Placements.Count;
|
||||
var attached = new SortedSet<int>();
|
||||
for (var part = 0; part < count; part++)
|
||||
{
|
||||
if (snapshot.Placements[part].IsCutOff)
|
||||
continue;
|
||||
foreach (var prerequisite in snapshot.Dependencies.PrerequisitesOf(part))
|
||||
if (snapshot.Placements[prerequisite] is { IsCutOff: true, SeparatesSheet: false })
|
||||
attached.Add(prerequisite);
|
||||
}
|
||||
return attached.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The planned order with every attached cut-off inserted, or null when one fits in no gap
|
||||
/// before its first part. <paramref name="countExpansion"/> is charged once per gap tried.
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<FixedProgramPlacement> Insert(CuttingPlanSnapshot snapshot,
|
||||
IReadOnlyList<FixedProgramPlacement> planned, IReadOnlyList<int> cutOffs,
|
||||
Action<FixedProgramPlacement> countExpansion, CancellationToken token)
|
||||
{
|
||||
if (cutOffs.Count == 0)
|
||||
return planned;
|
||||
var count = planned.Count;
|
||||
// The contours finished before each gap, and where the tool is when it reaches the gap.
|
||||
var states = new ReleasedContourState[count + 1];
|
||||
var arrivals = new Vector[count + 1];
|
||||
states[0] = new ReleasedContourState(reportMissingLeadIns: false);
|
||||
arrivals[0] = snapshot.StartPoint;
|
||||
for (var gap = 0; gap < count; gap++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var placement = planned[gap];
|
||||
states[gap + 1] = states[gap].Copy();
|
||||
states[gap + 1].Check(placement.Execution, arrivals[gap], placement.SourceOrdinal + 1,
|
||||
placement.IsCutOff, placement.FixedMotions(placement.Execution), token);
|
||||
arrivals[gap + 1] = placement.Execution.DeparturePoint;
|
||||
}
|
||||
var position = new Dictionary<int, int>();
|
||||
for (var index = 0; index < count; index++)
|
||||
position[planned[index].SourceOrdinal] = index;
|
||||
var latest = cutOffs.ToDictionary(cutOff => cutOff, cutOff => Enumerable.Range(0, snapshot.Placements.Count)
|
||||
.Where(part => position.ContainsKey(part) && snapshot.Dependencies.PrerequisitesOf(part).Contains(cutOff))
|
||||
.Select(part => position[part]).DefaultIfEmpty(count).Min());
|
||||
|
||||
var chains = new List<FixedProgramPlacement>[count + 1];
|
||||
// Latest gap first: a segment due late never takes a gap an earlier one needs.
|
||||
foreach (var cutOff in cutOffs.OrderByDescending(c => latest[c]).ThenBy(c => c))
|
||||
{
|
||||
var cut = snapshot.Placements[cutOff];
|
||||
var placed = false;
|
||||
for (var gap = latest[cutOff]; gap >= 0 && !placed; gap--)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
countExpansion(cut);
|
||||
var chain = chains[gap];
|
||||
var arrival = chain is { Count: > 0 } ? chain[^1].Execution.DeparturePoint : arrivals[gap];
|
||||
if (states[gap].Copy().Check(cut.Execution, arrival, cut.SourceOrdinal + 1, true, null, token).Strict.Count != 0)
|
||||
continue;
|
||||
if (gap < count)
|
||||
{
|
||||
var next = planned[gap];
|
||||
var findings = states[gap].Copy().Check(next.Execution, cut.Execution.DeparturePoint,
|
||||
next.SourceOrdinal + 1, next.IsCutOff, next.FixedMotions(next.Execution), token).Strict;
|
||||
if (findings.Count != 0)
|
||||
continue;
|
||||
}
|
||||
(chains[gap] ??= []).Add(cut);
|
||||
placed = true;
|
||||
}
|
||||
if (!placed)
|
||||
return null;
|
||||
}
|
||||
var order = new List<FixedProgramPlacement>(snapshot.Placements.Count);
|
||||
for (var gap = 0; gap <= count; gap++)
|
||||
{
|
||||
if (chains[gap] != null)
|
||||
order.AddRange(chains[gap]);
|
||||
if (gap < count)
|
||||
order.Add(planned[gap]);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,9 @@ public static class CuttingPlanService
|
||||
foreach (var cutOff in request.Plate?.CutOffs ?? [])
|
||||
if (cutOff?.Drawing != null)
|
||||
definitions[cutOff.Drawing] = cutOff;
|
||||
// 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);
|
||||
var nodes = new List<DependencyNode>();
|
||||
// Reuse the reader's coordinate validation without publishing its native kernel.
|
||||
var identities = new HashSet<Part>(ReferenceEqualityComparer.Instance);
|
||||
@@ -79,11 +82,22 @@ public static class CuttingPlanService
|
||||
if (!cut.HasCuttingContour)
|
||||
throw new ArgumentException("Placed cutoff program has no nonzero cutting motions.");
|
||||
var defined = definitions.TryGetValue(source.BaseDrawing, out var definition);
|
||||
var separates = defined && CutOff.SeparatesSheet(source, definition,
|
||||
request.Plate.BoundingBox(includeParts: false));
|
||||
nodes.Add(new(true, defined
|
||||
? new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit) : null,
|
||||
source.BoundingBox, null, null, separates));
|
||||
var sheet = request.Plate.BoundingBox(includeParts: false);
|
||||
var separates = defined && CutOff.SeparatesSheet(source, definition, sheet);
|
||||
CutOffDefinition nominal = null;
|
||||
if (defined)
|
||||
{
|
||||
if (!spans.TryGetValue(definition, out var segments))
|
||||
{
|
||||
spans[definition] = segments = new(ReferenceEqualityComparer.Instance);
|
||||
foreach (var segment in CutOffSegments.Of(definition, request.Parts, sheet))
|
||||
segments[segment.Part] = (segment.SpanStart, segment.SpanEnd);
|
||||
}
|
||||
nominal = segments.TryGetValue(source, out var span)
|
||||
? new(definition.Axis, definition.Position, span.Start, span.End)
|
||||
: new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit);
|
||||
}
|
||||
nodes.Add(new(true, nominal, source.BoundingBox, null, null, separates));
|
||||
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
|
||||
cut, OwnedProgramCopy.Copy(source.Program, token))
|
||||
{ IsCutOff = true, SeparatesSheet = separates });
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace OpenNest.Engine.CuttingPlanning;
|
||||
/// when a part on it cannot be reached without crossing parts already cut, the search learns
|
||||
/// "cut this part before those", keeps the parts cut before them and re-plans the rest. Once
|
||||
/// nothing new can be learned, the remaining budget goes to a full search over every ready part.
|
||||
/// Cut-off segments that must precede parts are left out of that search and placed into its
|
||||
/// plan afterwards (<see cref="CutOffInsertion"/>); only when one fits nowhere are they searched
|
||||
/// along with the parts.
|
||||
/// </summary>
|
||||
internal static class JointCuttingPlanSearch
|
||||
{
|
||||
@@ -33,11 +36,41 @@ internal static class JointCuttingPlanSearch
|
||||
|
||||
internal static Outcome Run(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
var walk = new Walk(snapshot, token);
|
||||
if (snapshot.PreservePartOrder)
|
||||
return Run(snapshot, token, new Walk(snapshot, token, []), []);
|
||||
// The parts are planned first, as if the attached cut-off segments were not there, and
|
||||
// the segments are placed into that plan. Only when one fits in no gap before its first
|
||||
// part is the whole plate searched again with the segments among the parts.
|
||||
var attached = CutOffInsertion.Attached(snapshot);
|
||||
var walk = new Walk(snapshot, token, attached);
|
||||
var outcome = Run(snapshot, token, walk, attached);
|
||||
if (attached.Length == 0 || outcome.Status != CuttingPlanStatus.Ready)
|
||||
return outcome;
|
||||
try
|
||||
{
|
||||
var inserted = CutOffInsertion.Insert(snapshot, outcome.Order, attached, walk.CountExpansion, token);
|
||||
if (inserted != null)
|
||||
return walk.Ready(inserted);
|
||||
}
|
||||
catch (BudgetExceededException)
|
||||
{
|
||||
return new(CuttingPlanStatus.NoSolutionWithinBudget, [], walk.Rejected, walk.Expansions, walk.LeadPrechecks);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new(CuttingPlanStatus.Cancelled, [], [], walk.Expansions, walk.LeadPrechecks);
|
||||
}
|
||||
return Run(snapshot, token, new Walk(snapshot, token, [], walk.Expansions, walk.LeadPrechecks), []);
|
||||
}
|
||||
|
||||
// Plans every placement but the excluded cut-offs, which count as cut from the start.
|
||||
private static Outcome Run(CuttingPlanSnapshot snapshot, CancellationToken token, Walk walk, int[] excluded)
|
||||
{
|
||||
var count = snapshot.Placements.Count;
|
||||
var maxContours = snapshot.Placements.Max(p => p.Prepared?.Count ?? 1);
|
||||
var stall = StallExpansionsPerEntry * snapshot.MaxEntries * maxContours;
|
||||
var preferEndpoints = count > 1 && maxContours > 1;
|
||||
var planned = Enumerable.Range(0, count).Except(excluded).ToArray();
|
||||
try
|
||||
{
|
||||
if (snapshot.PreservePartOrder)
|
||||
@@ -58,7 +91,7 @@ internal static class JointCuttingPlanSearch
|
||||
|
||||
var centres = snapshot.Placements.Select(Centre).ToArray();
|
||||
var prerequisites = Enumerable.Range(0, count)
|
||||
.Select(i => new HashSet<int>(snapshot.Dependencies.PrerequisitesOf(i))).ToArray();
|
||||
.Select(i => new HashSet<int>(snapshot.Dependencies.PrerequisitesOf(i).Except(excluded))).ToArray();
|
||||
|
||||
var nesting = NestedOrder(snapshot, centres, prerequisites, token);
|
||||
// A large aligned grid with no required order is toured row by row; anything else,
|
||||
@@ -67,7 +100,7 @@ internal static class JointCuttingPlanSearch
|
||||
var sequence = (prerequisites.All(p => p.Count == 0) && extents.All(e => e != null)
|
||||
&& !snapshot.Placements.Any(p => p.IsCutOff)
|
||||
? CuttingGridOrder.Plan(extents, snapshot.StartPoint, token) : null)
|
||||
?? CuttingNestedOrder.Plan(Enumerable.Range(0, count).ToArray(), snapshot.StartPoint, nesting);
|
||||
?? CuttingNestedOrder.Plan(planned, snapshot.StartPoint, nesting);
|
||||
Node resume = null;
|
||||
while (true)
|
||||
{
|
||||
@@ -125,7 +158,7 @@ internal static class JointCuttingPlanSearch
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<FixedProgramPlacement> SafestRoute(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
var walk = new Walk(snapshot, token);
|
||||
var walk = new Walk(snapshot, token, []);
|
||||
try
|
||||
{
|
||||
return walk.SafestRoute();
|
||||
@@ -223,9 +256,12 @@ internal static class JointCuttingPlanSearch
|
||||
/// The forward DFS over the next part on a given order and its emitted contour prefixes.
|
||||
/// Attempts share one expansion budget and one list of rejected findings.
|
||||
/// </summary>
|
||||
private sealed class Walk(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
private sealed class Walk(CuttingPlanSnapshot snapshot, CancellationToken token, int[] excluded,
|
||||
int expansions = 0, int leadPrechecks = 0)
|
||||
{
|
||||
private readonly List<CuttingPlanFinding> rejected = [];
|
||||
// Placements this walk never cuts and counts as cut from the start (attached cut-offs).
|
||||
private readonly HashSet<int> excluded = [.. excluded];
|
||||
private readonly LeadMaterialSnapshot[] materials =
|
||||
snapshot.Placements.Where(p => !p.IsCutOff).Select(p => p.Material).ToArray();
|
||||
// One feasibility adapter per source part per captured planning attempt: verdicts
|
||||
@@ -240,10 +276,10 @@ internal static class JointCuttingPlanSearch
|
||||
private ScrapPierceSearch scrapPierces;
|
||||
private readonly Dictionary<int, Entity[]> hostContours = [];
|
||||
|
||||
internal int Expansions { get; private set; }
|
||||
internal int Expansions { get; private set; } = expansions;
|
||||
|
||||
/// <summary>Lead precheck evaluations, tracked apart from DFS expansions: they are work, not free.</summary>
|
||||
internal int LeadPrechecks { get; private set; }
|
||||
internal int LeadPrechecks { get; private set; } = leadPrechecks;
|
||||
|
||||
internal IReadOnlyList<CuttingPlanFinding> Rejected => rejected.Distinct().ToArray();
|
||||
|
||||
@@ -283,7 +319,7 @@ internal static class JointCuttingPlanSearch
|
||||
token.ThrowIfCancellationRequested();
|
||||
var frame = stack.Peek();
|
||||
var node = frame.Node;
|
||||
if (node.Order.Length == snapshot.Placements.Count)
|
||||
if (node.Order.Length == snapshot.Placements.Count - excluded.Count)
|
||||
{
|
||||
attempt.Order = node.Order;
|
||||
return attempt;
|
||||
@@ -346,24 +382,27 @@ internal static class JointCuttingPlanSearch
|
||||
/// part. Supplied order: the next not-yet-finished part in that order (the sequence
|
||||
/// is re-read after every learned-order replan). Sequence-free fallback: nearest
|
||||
/// dependency-ready remaining part once the current part counts as finished, stable
|
||||
/// ordinal ties. Never the current or a finished part. Global coordinates.
|
||||
/// ordinal ties. Never the current or a finished part, and never a cut-off segment: a
|
||||
/// segment is a short fixed cut on the way to the part after it, which the entry
|
||||
/// should face, so a kept order with segments gets the entries planned without them.
|
||||
/// Global coordinates.
|
||||
/// </summary>
|
||||
private Vector? LookAheadCentre(Node node, int[] sequence, FixedProgramPlacement source) =>
|
||||
NextCut(node, sequence, source) is { } next ? Facing(source, next) : null;
|
||||
|
||||
private FixedProgramPlacement NextCut(Node node, int[] sequence, FixedProgramPlacement source)
|
||||
{
|
||||
var finished = node.Order.Select(o => o.SourceOrdinal).ToHashSet();
|
||||
var finished = Finished(node);
|
||||
finished.Add(source.SourceOrdinal);
|
||||
if (sequence != null)
|
||||
for (var i = node.Order.Length + 1; i < sequence.Length; i++)
|
||||
if (!finished.Contains(sequence[i]))
|
||||
if (!finished.Contains(sequence[i]) && !IsSegment(snapshot.Placements[sequence[i]]))
|
||||
return snapshot.Placements[sequence[i]];
|
||||
var from = Centre(source);
|
||||
FixedProgramPlacement best = null;
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
foreach (var candidate in snapshot.Placements
|
||||
.Where(p => !finished.Contains(p.SourceOrdinal)
|
||||
.Where(p => !finished.Contains(p.SourceOrdinal) && !IsSegment(p)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, finished))
|
||||
.OrderBy(p => p.SourceOrdinal))
|
||||
{
|
||||
@@ -377,6 +416,10 @@ internal static class JointCuttingPlanSearch
|
||||
return best;
|
||||
}
|
||||
|
||||
// A non-separating cut-off: one trimmed segment of a line, never a look-ahead target.
|
||||
private static bool IsSegment(FixedProgramPlacement placement) =>
|
||||
placement is { IsCutOff: true, SeparatesSheet: false };
|
||||
|
||||
// Where the tool may have to go next, in global coordinates: the look-ahead point and,
|
||||
// for an ordinary next part, the corners of its cut extent. Its pierce can be anywhere
|
||||
// on it, so an outside entry that sees all of them leaves the most ways open.
|
||||
@@ -450,7 +493,7 @@ internal static class JointCuttingPlanSearch
|
||||
sources = [active.Source];
|
||||
else
|
||||
{
|
||||
var finished = node.Order.Select(o => o.SourceOrdinal).ToHashSet();
|
||||
var finished = Finished(node);
|
||||
sources = sequence == null
|
||||
? snapshot.Placements.Where(p => !finished.Contains(p.SourceOrdinal)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, finished))
|
||||
@@ -467,7 +510,10 @@ internal static class JointCuttingPlanSearch
|
||||
CountExpansion(source);
|
||||
var checker = node.Checker.Copy();
|
||||
if (Check(source, source.Execution, node.Position, checker, attempt))
|
||||
yield return new(new([.. node.Order, source], source.Execution.DeparturePoint, checker, null, node),
|
||||
// A segment is transparent: the part after it ranks its entries and hole route
|
||||
// from where the tool was before the segment, as planned without it.
|
||||
yield return new(new([.. node.Order, source], source.Execution.DeparturePoint, checker, null, node,
|
||||
IsSegment(source) ? node.RoutePosition : null),
|
||||
source.Execution.RapidDistanceFrom(node.Position), source.SourceOrdinal, -1, -1);
|
||||
continue;
|
||||
}
|
||||
@@ -595,7 +641,7 @@ internal static class JointCuttingPlanSearch
|
||||
// Placement origins are authored program coordinates, not a downstream cut.
|
||||
// Use the same material-centre look-ahead as the retained entry search.
|
||||
var ranked = prepared.AutomaticEntryCandidatesWithFallbacks(prepared.PerimeterOrdinal, token: token)
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(target ?? node.Position - source.Location)).ToArray();
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(target ?? node.RoutePosition - source.Location)).ToArray();
|
||||
var perimeters = ranked.Where(c => Evaluate(source, c).Status == ContourFeasibilityStatus.Clear)
|
||||
.Select(c => c.Choice).ToArray();
|
||||
if (perimeters.Length == 0)
|
||||
@@ -745,7 +791,7 @@ internal static class JointCuttingPlanSearch
|
||||
FixedProgramPlacement source, int contour)
|
||||
{
|
||||
var prepared = source.Prepared!;
|
||||
var arrival = node.Position - source.Location;
|
||||
var arrival = node.RoutePosition - source.Location;
|
||||
// One global->local conversion of target and arrival; geometry is already rotated.
|
||||
Vector? local = LookAheadCentre(node, sequence, source) is { } target
|
||||
? target - source.Location : null;
|
||||
@@ -923,7 +969,7 @@ internal static class JointCuttingPlanSearch
|
||||
var source = active.Source;
|
||||
var prepared = source.Prepared;
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
||||
var localArrival = active.Arrival - source.Location;
|
||||
var localArrival = active.Boundary.RoutePosition - source.Location;
|
||||
var centres = CuttingHoleOrder.Centres(prepared, localArrival, token);
|
||||
var endpoint = perimeter.Point;
|
||||
var entries = new Dictionary<int, ContourChoice>();
|
||||
@@ -957,7 +1003,15 @@ internal static class JointCuttingPlanSearch
|
||||
return new(route, entries, pierces, endpoint);
|
||||
}
|
||||
|
||||
private void CountExpansion(FixedProgramPlacement source)
|
||||
// The placements cut at a node, and the excluded ones, which count as cut throughout.
|
||||
private HashSet<int> Finished(Node node)
|
||||
{
|
||||
var finished = node.Order.Select(o => o.SourceOrdinal).ToHashSet();
|
||||
finished.UnionWith(excluded);
|
||||
return finished;
|
||||
}
|
||||
|
||||
internal void CountExpansion(FixedProgramPlacement source)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (Expansions == snapshot.ExpansionBudget)
|
||||
@@ -1195,9 +1249,16 @@ internal static class JointCuttingPlanSearch
|
||||
private sealed record ActivePart(FixedProgramPlacement Source, ContourChoice[] Choices, Vector Arrival,
|
||||
ReleasedContourState Before, double Distance, Node Boundary,
|
||||
ContourChoice Perimeter = null, HolePreference Preference = null);
|
||||
/// <summary>A search state; Previous links a whole-part boundary to the boundary before it.</summary>
|
||||
/// <summary>
|
||||
/// A search state; Previous links a whole-part boundary to the boundary before it. Anchor,
|
||||
/// when set, is where the tool was before the cut-off segments cut last: entries and hole
|
||||
/// routes rank from <see cref="RoutePosition"/>, every rapid is checked from Position.
|
||||
/// </summary>
|
||||
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker,
|
||||
ActivePart Active, Node Previous);
|
||||
ActivePart Active, Node Previous, Vector? Anchor = null)
|
||||
{
|
||||
internal Vector RoutePosition => Anchor ?? Position;
|
||||
}
|
||||
/// <summary>Rank is the automatic selection slot (entry order) inside its contour stage; int.MinValue for legacy children.</summary>
|
||||
private sealed record Edge(Node Node, double Distance, int Ordinal, int Contour, int Entry,
|
||||
int Rank = int.MinValue, int ContourRank = 0);
|
||||
|
||||
@@ -37,28 +37,31 @@ namespace OpenNest.Engine.Sequencing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the given cutoffs' parts to where the cutoff rules need them while every other
|
||||
/// part and cutoff keeps its position: just before the first part each nominal line
|
||||
/// crosses, or last when it splits the whole sheet. One that crosses nothing stays put,
|
||||
/// unless it would follow a sheet-separating cutoff.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void PlaceCutOffs(Plate plate, IEnumerable<CutOff> definitions)
|
||||
{
|
||||
var bounds = plate.BoundingBox(includeParts: false);
|
||||
var moving = definitions
|
||||
.Select(definition => (Definition: definition, Part: plate.Parts.FirstOrDefault(part =>
|
||||
ReferenceEquals(part.BaseDrawing, definition.Drawing))))
|
||||
.Where(item => item.Part != null).ToList();
|
||||
.SelectMany(definition => CutOffSegments.Of(plate, definition)
|
||||
.Select(segment => (Definition: definition, Segment: segment)))
|
||||
.ToList();
|
||||
if (moving.Count == 0)
|
||||
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 separating = new List<Part>();
|
||||
foreach (var (definition, part) in moving)
|
||||
foreach (var (definition, segment) in moving)
|
||||
{
|
||||
var part = segment.Part;
|
||||
if (CutOff.SeparatesSheet(part, definition, bounds))
|
||||
{
|
||||
order.Remove(part);
|
||||
@@ -67,9 +70,9 @@ namespace OpenNest.Engine.Sequencing
|
||||
}
|
||||
var target = order.FindIndex(other => !other.BaseDrawing.IsCutOff
|
||||
&& CuttingDependencyGraph.CutOffCrosses(definition.Axis, definition.Position,
|
||||
definition.StartLimit, definition.EndLimit, other.BoundingBox, bounds));
|
||||
segment.SpanStart, segment.SpanEnd, other.BoundingBox, bounds));
|
||||
if (target < 0)
|
||||
target = order.FindIndex(other => other.BaseDrawing.IsCutOff && !moving.Any(m => ReferenceEquals(m.Part, other))
|
||||
target = order.FindIndex(other => other.BaseDrawing.IsCutOff && !movingParts.Contains(other)
|
||||
&& existing.TryGetValue(other.BaseDrawing, out var cut) && CutOff.SeparatesSheet(other, cut, bounds));
|
||||
if (target < 0 || target > order.IndexOf(part))
|
||||
continue;
|
||||
@@ -79,11 +82,11 @@ namespace OpenNest.Engine.Sequencing
|
||||
order.AddRange(separating);
|
||||
|
||||
// Touch only the moved cutoff parts; real parts never leave the plate.
|
||||
foreach (var (_, part) in moving)
|
||||
foreach (var part in movingParts)
|
||||
plate.Parts.Remove(part);
|
||||
for (var index = 0; index < order.Count; index++)
|
||||
{
|
||||
if (moving.Any(item => ReferenceEquals(item.Part, order[index])))
|
||||
if (movingParts.Contains(order[index]))
|
||||
plate.Parts.Insert(index, order[index]);
|
||||
}
|
||||
}
|
||||
@@ -101,6 +104,11 @@ namespace OpenNest.Engine.Sequencing
|
||||
foreach (var cutOff in plate.CutOffs)
|
||||
definitions[cutOff.Drawing] = cutOff;
|
||||
var bounds = plate.BoundingBox(includeParts: false);
|
||||
// Each cutoff part cuts one segment of its line and precedes the parts in that segment's span.
|
||||
var spans = new Dictionary<Part, (double Start, double End)>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var cutOff in plate.CutOffs)
|
||||
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.
|
||||
@@ -117,6 +125,12 @@ namespace OpenNest.Engine.Sequencing
|
||||
.Where(item => item.Part.BaseDrawing.IsCutOff).ToList();
|
||||
if (cuts.Count == 0)
|
||||
return ordered.Concat(separating).ToList();
|
||||
// Cutoffs keep the route's order; within one cutoff, the segments due before the same
|
||||
// part keep the order they were regenerated in (the line's program order).
|
||||
var firstOnRoute = cuts.GroupBy(item => item.Part.BaseDrawing)
|
||||
.ToDictionary(group => group.Key, group => group.Min(item => item.Index));
|
||||
cuts = cuts.OrderBy(item => firstOnRoute[item.Part.BaseDrawing])
|
||||
.ThenBy(item => plate.Parts.IndexOf(item.Part)).ToList();
|
||||
|
||||
var emitted = new bool[ordered.Count];
|
||||
var result = new List<Part>(ordered.Count);
|
||||
@@ -133,8 +147,9 @@ namespace OpenNest.Engine.Sequencing
|
||||
// An orphaned cutoff still must not follow potentially
|
||||
// crossed parts when its nominal span cannot be recovered.
|
||||
if (!definitions.TryGetValue(cut.Part.BaseDrawing, out var definition)
|
||||
|| !spans.TryGetValue(cut.Part, out var span)
|
||||
|| CuttingDependencyGraph.CutOffCrosses(definition.Axis, definition.Position,
|
||||
definition.StartLimit, definition.EndLimit, part.BoundingBox, bounds))
|
||||
span.Start, span.End, part.BoundingBox, bounds))
|
||||
{
|
||||
result.Add(cut.Part);
|
||||
emitted[cut.Index] = true;
|
||||
|
||||
@@ -610,9 +610,19 @@ namespace OpenNest.IO
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based place in the plate's cut sequence (<c>Plate.Parts</c> order,
|
||||
/// cut-offs included). Null in older files, which load it at the end.
|
||||
/// cut-offs included) of the cut-off's first segment. Null in older files, which
|
||||
/// load it at the end.
|
||||
/// </summary>
|
||||
public int? Sequence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The place of each of the cut-off's trimmed segments in the cut sequence, in
|
||||
/// ascending order along its line, when they are not consecutive. Omitted for one
|
||||
/// segment or consecutive segments; readers without it place the segments
|
||||
/// consecutively from <see cref="Sequence"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int[] Segments { get; init; }
|
||||
}
|
||||
|
||||
public record SizeDto
|
||||
|
||||
@@ -644,7 +644,7 @@ namespace OpenNest.IO
|
||||
// Cut-offs
|
||||
if (p.CutOffs != null)
|
||||
{
|
||||
var sequence = new Dictionary<CutOff, int>();
|
||||
var sequence = new Dictionary<CutOff, IReadOnlyList<int>>();
|
||||
|
||||
foreach (var cutoffDto in p.CutOffs)
|
||||
{
|
||||
@@ -659,8 +659,10 @@ namespace OpenNest.IO
|
||||
};
|
||||
plate.CutOffs.Add(cutoff);
|
||||
|
||||
if (cutoffDto.Sequence is int index)
|
||||
sequence[cutoff] = index;
|
||||
if (cutoffDto.Segments is { Length: > 0 } segments)
|
||||
sequence[cutoff] = segments;
|
||||
else if (cutoffDto.Sequence is int index)
|
||||
sequence[cutoff] = [index];
|
||||
}
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings(), sequence);
|
||||
|
||||
@@ -368,15 +368,13 @@ namespace OpenNest.IO
|
||||
var cutoffs = new List<CutOffDto>();
|
||||
foreach (var cutoff in plate.CutOffs)
|
||||
{
|
||||
var sequence = -1;
|
||||
for (var j = 0; j < plate.Parts.Count; j++)
|
||||
{
|
||||
if (ReferenceEquals(plate.Parts[j].BaseDrawing, cutoff.Drawing))
|
||||
{
|
||||
sequence = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Each trimmed segment is its own part with its own place in the sequence.
|
||||
var segments = CutOffSegments.Of(plate, cutoff)
|
||||
.Select(segment => plate.Parts.IndexOf(segment.Part))
|
||||
.Where(index => index >= 0)
|
||||
.ToArray();
|
||||
var consecutive = segments.Length == 0
|
||||
|| segments.SequenceEqual(Enumerable.Range(segments.Min(), segments.Length));
|
||||
|
||||
cutoffs.Add(
|
||||
new CutOffDto
|
||||
@@ -386,7 +384,8 @@ namespace OpenNest.IO
|
||||
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
|
||||
StartLimit = cutoff.StartLimit,
|
||||
EndLimit = cutoff.EndLimit,
|
||||
Sequence = sequence >= 0 ? sequence : null,
|
||||
Sequence = segments.Length > 0 ? segments.Min() : null,
|
||||
Segments = consecutive ? null : segments,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ public class PlateRapidEnumeratorTests
|
||||
|
||||
// Two interrupted skeleton cuts followed by an uninterrupted tail separator.
|
||||
var cutoffs = plate.Parts.Where(p => p.BaseDrawing.IsCutOff).ToArray();
|
||||
Assert.Equal(new[] { 2, 2, 1 }, cutoffs.Select(p => p.Program.Codes.OfType<LinearMove>().Count()));
|
||||
Assert.Equal(new[] { 2, 2, 1 }, plate.CutOffs.Select(c => TestHelpers.CutOffParts(plate, c).Length));
|
||||
Assert.All(cutoffs, p => Assert.Single(p.Program.Codes.OfType<LinearMove>()));
|
||||
var before = plate.Parts.Select(p => NestWriter.GetProgramText(p.Program)).ToArray();
|
||||
var expected = new List<RapidEnumerator.Segment>();
|
||||
var position = Vector.Zero;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class AutomaticCutOffBatchTests
|
||||
Assert.Same(manual.Drawing, plates[0].Parts[0].BaseDrawing);
|
||||
Assert.Same(realParts[0], plates[0].Parts[1]);
|
||||
Assert.Same(realParts[1], plates[1].Parts[0]);
|
||||
Assert.All(plates.Take(2), p => Assert.Equal(p.CutOffs.Count, p.Parts.Count(x => x.BaseDrawing.IsCutOff)));
|
||||
Assert.All(plates.Take(2), p => Assert.Equal(TestHelpers.CutOffPartCount(p), p.Parts.Count(x => x.BaseDrawing.IsCutOff)));
|
||||
Assert.All(plates[1].CutOffs, c => Assert.True(c.Position.X < 0));
|
||||
var tailProgram = plates[1].Parts[^1].Program;
|
||||
Assert.Equal(50, tailProgram.BoundingBox().Width, 6);
|
||||
|
||||
@@ -38,14 +38,15 @@ public class AutomaticCutOffLifecycleTests
|
||||
Assert.Equal(new[] { manual }.Concat(plan.Definitions), plate.CutOffs);
|
||||
Assert.Equal(manualDefinition, Definition(manual));
|
||||
AssertSequence(plate, realParts[0].Part, realParts[1].Part);
|
||||
AssertOnePartPerDefinition(plate);
|
||||
AssertOnePartPerSegment(plate);
|
||||
AssertRealPartsUnchanged(plate, realParts);
|
||||
for (var i = 0; i < plan.Definitions.Count; i++)
|
||||
{
|
||||
var materialized = plate.Parts[i + 3];
|
||||
Assert.Same(plan.Definitions[i].Drawing, materialized.BaseDrawing);
|
||||
Assert.NotSame(plan.PreviewParts[i], materialized);
|
||||
Assert.Equal(previewPrograms[i], NestWriter.GetProgramText(materialized.Program));
|
||||
// One part per trimmed segment, which together cut what the preview part cut.
|
||||
var materialized = TestHelpers.CutOffParts(plate, plan.Definitions[i]);
|
||||
Assert.NotEmpty(materialized);
|
||||
Assert.All(materialized, p => Assert.DoesNotContain(p, plan.PreviewParts));
|
||||
Assert.Equal(previewPrograms[i], NestWriter.GetProgramText(TestHelpers.CutOffProgram(plate, plan.Definitions[i])));
|
||||
}
|
||||
|
||||
var repeated = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 35 }, settings);
|
||||
@@ -62,7 +63,7 @@ public class AutomaticCutOffLifecycleTests
|
||||
Apply(plate, repeated, settings);
|
||||
Assert.Equal(new[] { manual }.Concat(plan.Definitions), plate.CutOffs);
|
||||
AssertSequence(plate, realParts[0].Part, realParts[1].Part);
|
||||
AssertOnePartPerDefinition(plate);
|
||||
AssertOnePartPerSegment(plate);
|
||||
AssertRealPartsUnchanged(plate, realParts);
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ public class AutomaticCutOffLifecycleTests
|
||||
Assert.Equal(realParts.Select(p => p.ProgramText), loadedRealParts.Select(p => NestWriter.GetProgramText(p.Program)));
|
||||
Assert.Equal(realParts.Select(p => p.Rotation), loadedRealParts.Select(p => p.Rotation));
|
||||
AssertSequence(loadedPlate, loadedRealParts[0], loadedRealParts[1]);
|
||||
AssertOnePartPerDefinition(loadedPlate);
|
||||
AssertOnePartPerSegment(loadedPlate);
|
||||
var loadedDrawing = Assert.Single(loaded.Drawings);
|
||||
Assert.False(loadedDrawing.IsCutOff);
|
||||
Assert.All(loadedRealParts, p => Assert.Same(loadedDrawing, p.BaseDrawing));
|
||||
@@ -110,7 +111,7 @@ public class AutomaticCutOffLifecycleTests
|
||||
loadedPlate.RegenerateCutOffs(settings);
|
||||
Assert.Equal(definitions, loadedPlate.CutOffs.Select(Definition));
|
||||
AssertSequence(loadedPlate, loadedRealParts[0], loadedRealParts[1]);
|
||||
AssertOnePartPerDefinition(loadedPlate);
|
||||
AssertOnePartPerSegment(loadedPlate);
|
||||
AssertRealPartsUnchanged(loadedPlate, loadedState);
|
||||
}
|
||||
|
||||
@@ -159,12 +160,12 @@ public class AutomaticCutOffLifecycleTests
|
||||
Assert.Equal(definitions, plate.CutOffs.ToArray());
|
||||
Assert.Equal(definitionValues, plate.CutOffs.Select(Definition));
|
||||
AssertSequence(plate, realParts[0].Part, moved);
|
||||
AssertOnePartPerDefinition(plate);
|
||||
AssertOnePartPerSegment(plate);
|
||||
AssertRealPartsUnchanged(plate, movedState);
|
||||
AssertRealPartsUnchanged(plate, new[] { realParts[0], realParts[1] with { Location = moved.Location } });
|
||||
Assert.NotSame(separatorPart, plate.Parts[^1]);
|
||||
Assert.DoesNotContain(separatorPart, plate.Parts);
|
||||
var after = Segments(plate.Parts[^1]);
|
||||
var after = TestHelpers.CutOffParts(plate, separator).Select(p => Assert.Single(Segments(p))).ToArray();
|
||||
Assert.Equal(2, after.Length);
|
||||
Assert.All(after, s =>
|
||||
{
|
||||
@@ -180,7 +181,7 @@ public class AutomaticCutOffLifecycleTests
|
||||
Assert.Equal(regeneratedPrograms, plate.Parts.Where(p => p.BaseDrawing.IsCutOff)
|
||||
.Select(p => NestWriter.GetProgramText(p.Program)));
|
||||
AssertSequence(plate, realParts[0].Part, moved);
|
||||
AssertOnePartPerDefinition(plate);
|
||||
AssertOnePartPerSegment(plate);
|
||||
AssertRealPartsUnchanged(plate, movedState);
|
||||
AssertCutsAvoidRectangles(plate, settings.PartClearance);
|
||||
}
|
||||
@@ -251,23 +252,31 @@ public class AutomaticCutOffLifecycleTests
|
||||
plate.RegenerateCutOffs(settings);
|
||||
}
|
||||
|
||||
private static void AssertSequence(Plate plate, Part first, Part second) =>
|
||||
Assert.Collection(plate.Parts,
|
||||
p => Assert.Same(first, p),
|
||||
p => Assert.Same(plate.CutOffs[0].Drawing, p.BaseDrawing),
|
||||
p => Assert.Same(second, p),
|
||||
p => Assert.Same(plate.CutOffs[1].Drawing, p.BaseDrawing),
|
||||
p => Assert.Same(plate.CutOffs[2].Drawing, p.BaseDrawing),
|
||||
p => Assert.Same(plate.CutOffs[3].Drawing, p.BaseDrawing));
|
||||
// The manual cut-off's segment between the real parts, then each planned cut-off's segments
|
||||
// in definition order; the planned line through the second part has two.
|
||||
private static void AssertSequence(Plate plate, Part first, Part second)
|
||||
{
|
||||
var expected = new List<Part> { first };
|
||||
expected.AddRange(TestHelpers.CutOffParts(plate, plate.CutOffs[0]));
|
||||
expected.Add(second);
|
||||
foreach (var definition in plate.CutOffs.Skip(1))
|
||||
expected.AddRange(TestHelpers.CutOffParts(plate, definition));
|
||||
Assert.Equal(expected, plate.Parts);
|
||||
Assert.Single(TestHelpers.CutOffParts(plate, plate.CutOffs[0]));
|
||||
Assert.Single(TestHelpers.CutOffParts(plate, plate.CutOffs[1]));
|
||||
Assert.Equal(2, TestHelpers.CutOffParts(plate, plate.CutOffs[2]).Length);
|
||||
}
|
||||
|
||||
private static void AssertOnePartPerDefinition(Plate plate)
|
||||
private static void AssertOnePartPerSegment(Plate plate)
|
||||
{
|
||||
Assert.Equal(4, plate.CutOffs.Count);
|
||||
Assert.Equal(4, plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.All(plate.CutOffs, c =>
|
||||
{
|
||||
Assert.True(c.Drawing.IsCutOff);
|
||||
Assert.Single(plate.Parts.Where(p => ReferenceEquals(p.BaseDrawing, c.Drawing)));
|
||||
var parts = TestHelpers.CutOffParts(plate, c);
|
||||
Assert.Equal(c.SegmentPrograms().Count, parts.Length);
|
||||
Assert.All(parts, p => Assert.Equal(2, p.Program.Codes.Count));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,7 +327,9 @@ public class AutomaticCutOffLifecycleTests
|
||||
Assert.Equal(realParts[i].Location.Y, savedParts[i].GetProperty("y").GetDouble());
|
||||
}
|
||||
var cuts = savedPlate.GetProperty("cutOffs").EnumerateArray().ToArray();
|
||||
Assert.Equal(new[] { 1, 3, 4, 5 }, cuts.Select(c => c.GetProperty("sequence").GetInt32()));
|
||||
// The line through the second part has two consecutive segments at 4 and 5.
|
||||
Assert.Equal(new[] { 1, 3, 4, 6 }, cuts.Select(c => c.GetProperty("sequence").GetInt32()));
|
||||
Assert.All(cuts, c => Assert.False(c.TryGetProperty("segments", out _)));
|
||||
}
|
||||
|
||||
private static (Vector From, Vector To)[] Segments(Part part)
|
||||
|
||||
@@ -657,8 +657,8 @@ public class AutomaticCutOffPlannerTests
|
||||
Assert.Equal(first.UsedSpan, second.UsedSpan);
|
||||
Assert.Equal(3, second.Diagnostics.Count(d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff));
|
||||
plate.RegenerateCutOffs(settings);
|
||||
Assert.Equal(4, plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.All(plate.CutOffs, c => Assert.Single(plate.Parts.Where(p => ReferenceEquals(p.BaseDrawing, c.Drawing))));
|
||||
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.All(plate.CutOffs, c => Assert.Equal(c.SegmentPrograms().Count, TestHelpers.CutOffParts(plate, c).Length));
|
||||
Assert.Same(manual.Drawing, plate.Parts[0].BaseDrawing);
|
||||
Assert.Same(real, plate.Parts[1]);
|
||||
Assert.Same(secondReal, plate.Parts[2]);
|
||||
@@ -934,15 +934,14 @@ public class AutomaticCutOffPlannerTests
|
||||
plate.CutOffs.Add(definition);
|
||||
plate.RegenerateCutOffs(settings);
|
||||
|
||||
var cuts = plate.Parts.Where(p => p.BaseDrawing.IsCutOff).ToArray();
|
||||
Assert.Equal(plan.Definitions.Count, cuts.Length);
|
||||
for (var i = 0; i < cuts.Length; i++)
|
||||
Assert.Equal(previewSegments[i], Segments(cuts[i].Program));
|
||||
var repeated = Segments(cuts[0].Program);
|
||||
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
for (var i = 0; i < plan.Definitions.Count; i++)
|
||||
Assert.Equal(previewSegments[i], Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[i])));
|
||||
var repeated = Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[0]));
|
||||
Assert.Equal(2, repeated.Length);
|
||||
Assert.Equal(20 - System.Math.Sqrt(75), repeated[0].To.Y, 8);
|
||||
Assert.Equal(20 + System.Math.Sqrt(75), repeated[1].From.Y, 8);
|
||||
var separator = Assert.Single(Segments(cuts[1].Program));
|
||||
var separator = Assert.Single(Segments(TestHelpers.CutOffProgram(plate, plan.Definitions[1])));
|
||||
Assert.Equal(new Vector(plan.TailSeparatorX!.Value, 0), separator.From);
|
||||
Assert.Equal(new Vector(plan.TailSeparatorX.Value, 81), separator.To);
|
||||
var arc = Assert.Single(ConvertProgram.ToGeometry(real.Program).OfType<Arc>());
|
||||
@@ -986,7 +985,7 @@ public class AutomaticCutOffPlannerTests
|
||||
Assert.True(second.HasSeparatedTail);
|
||||
Assert.Equal(first.TailSeparatorX, second.TailSeparatorX);
|
||||
Assert.Equal(first.TailLength, second.TailLength);
|
||||
Assert.Equal(3, plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.Equal(TestHelpers.CutOffPartCount(plate), plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -56,9 +56,10 @@ public class CutOffEdgeClearanceTests
|
||||
for (var iteration = 0; iteration < 2; iteration++)
|
||||
{
|
||||
plate.RegenerateCutOffs(settings);
|
||||
var cutPart = Assert.Single(plate.Parts.Where(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.Same(cutoff.Drawing, cutPart.BaseDrawing);
|
||||
AssertRectangleClearance(cutPart.Program, cutPart.Location, axis,
|
||||
var cutParts = plate.Parts.Where(p => p.BaseDrawing.IsCutOff).ToArray();
|
||||
Assert.All(cutParts, cutPart => Assert.Same(cutoff.Drawing, cutPart.BaseDrawing));
|
||||
Assert.All(cutParts, cutPart => Assert.Equal(2, cutPart.Program.Codes.Count)); // One segment each.
|
||||
AssertRectangleClearance(TestHelpers.CutOffProgram(plate, cutoff), Vector.Zero, axis,
|
||||
left, right, bottom, top, settings.PartClearance);
|
||||
}
|
||||
}
|
||||
@@ -118,11 +119,10 @@ public class CutOffEdgeClearanceTests
|
||||
var cutoff = new CutOff(Orient(new Vector(cutCoordinate, 0)), axis);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 });
|
||||
var cutPart = Assert.Single(plate.Parts.Where(p => p.BaseDrawing.IsCutOff));
|
||||
var codes = cutPart.Program.Codes;
|
||||
var codes = TestHelpers.CutOffProgram(plate, cutoff).Codes;
|
||||
Assert.Equal(4, codes.Count);
|
||||
var before = Assert.IsType<LinearMove>(codes[1]).EndPoint + cutPart.Location;
|
||||
var after = Assert.IsType<RapidMove>(codes[2]).EndPoint + cutPart.Location;
|
||||
var before = Assert.IsType<LinearMove>(codes[1]).EndPoint;
|
||||
var after = Assert.IsType<RapidMove>(codes[2]).EndPoint;
|
||||
Assert.Equal(occupiedStart, axis == CutOffAxis.Vertical ? before.Y : before.X, 9);
|
||||
Assert.Equal(occupiedEnd, axis == CutOffAxis.Vertical ? after.Y : after.X, 9);
|
||||
}
|
||||
|
||||
@@ -86,14 +86,13 @@ public class CutOffLeadClearanceTests
|
||||
|
||||
private static void AssertClearance(Plate plate, bool horizontal)
|
||||
{
|
||||
var cutPart = Assert.Single(plate.Parts.Where(p => p.BaseDrawing.IsCutOff));
|
||||
var codes = cutPart.Program.Codes;
|
||||
var codes = TestHelpers.CutOffProgram(plate, Assert.Single(plate.CutOffs)).Codes;
|
||||
Assert.Equal(4, codes.Count);
|
||||
// Independent material oracle: the unrotated outline is a union of three rectangles.
|
||||
// Undo the test's exact quarter-turn, not a production geometry/offset operation.
|
||||
Vector Local(Vector point)
|
||||
{
|
||||
point += cutPart.Location - new Vector(50, 10);
|
||||
point -= new Vector(50, 10);
|
||||
return horizontal ? new Vector(point.Y, -point.X) : point;
|
||||
}
|
||||
var rectangles = new[] { (0.0, 40.0, 0.0, 20.0), (0.0, 2.0, 20.0, 20.25), (38.0, 40.0, 20.0, 20.25) };
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CutOffs;
|
||||
|
||||
/// <summary>
|
||||
/// A cut-off's trimmed segments are materialized as one part each, with its own place in the
|
||||
/// cut sequence; each stands for the span of the line between its neighbouring segments.
|
||||
/// </summary>
|
||||
public class CutOffSegmentsTests
|
||||
{
|
||||
private const double Clearance = 0.02;
|
||||
|
||||
[Fact]
|
||||
public void Of_GivesEachSegmentTheSpanBetweenItsNeighbours()
|
||||
{
|
||||
var (plate, cutOff, _, _) = ThreeSegments();
|
||||
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
|
||||
Assert.Equal(3, segments.Count);
|
||||
AssertSegment(segments[0], 0, 10 - Clearance, 0, 20 + Clearance);
|
||||
AssertSegment(segments[1], 20 + Clearance, 30 - Clearance, 10 - Clearance, 40 + Clearance);
|
||||
AssertSegment(segments[2], 40 + Clearance, 60, 30 - Clearance, 60);
|
||||
Assert.Equal(segments.Select(s => s.Part), plate.Parts.Where(p => p.BaseDrawing.IsCutOff));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Of_LimitedLine_SpansEndAtItsLimits()
|
||||
{
|
||||
var plate = TestHelpers.MakePlate(60, 120, TestHelpers.MakePartAt(10, 10, 10));
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical) { StartLimit = 5, EndLimit = 45 };
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
|
||||
Assert.Equal(2, segments.Count);
|
||||
AssertSegment(segments[0], 5, 10 - Clearance, 5, 20 + Clearance);
|
||||
AssertSegment(segments[1], 20 + Clearance, 45, 10 - Clearance, 45);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Of_PartThatCannotBeMeasured_StandsForTheWholeLine()
|
||||
{
|
||||
var plate = TestHelpers.MakePlate(60, 120);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
var program = new Program(Mode.Incremental);
|
||||
program.Codes.Add(new RapidMove(15, 0));
|
||||
program.Codes.Add(new LinearMove(0, 60));
|
||||
cutOff.Drawing.Program = program;
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.Parts.Add(new Part(cutOff.Drawing));
|
||||
|
||||
var segment = Assert.Single(CutOffSegments.Of(plate, cutOff));
|
||||
|
||||
AssertSegment(segment, 0, 60, 0, 60);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_KeepsEachSegmentsPlaceInTheSequence()
|
||||
{
|
||||
var (plate, cutOff, lower, upper) = ThreeSegments();
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
plate.Parts.Reorder([segments[0], lower, segments[1], upper, segments[2]]);
|
||||
var settings = new CutOffSettings();
|
||||
|
||||
plate.RegenerateCutOffs(settings);
|
||||
|
||||
AssertArrangement(plate, cutOff, lower, upper);
|
||||
// A part moved along the line still sits in the same gap: its segments keep their places.
|
||||
lower.Offset(1, 0);
|
||||
plate.RegenerateCutOffs(settings);
|
||||
AssertArrangement(plate, cutOff, lower, upper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_SegmentWithoutASavedPlace_FollowsTheOneBeforeIt()
|
||||
{
|
||||
var (plate, cutOff, lower, upper) = ThreeSegments();
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
plate.Parts.Reorder([segments[0], lower, segments[1], upper, segments[2]]);
|
||||
// A part dropped onto the top segment splits it in two.
|
||||
var added = TestHelpers.MakePartAt(10, 48, 5);
|
||||
plate.Parts.Add(added);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
var regenerated = CutOffSegments.Of(plate, cutOff);
|
||||
Assert.Equal(4, regenerated.Count);
|
||||
Assert.Equal(new[] { regenerated[0].Part, lower, regenerated[1].Part, upper, regenerated[2].Part, regenerated[3].Part, added },
|
||||
plate.Parts);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CutDirection.AwayFromOrigin)]
|
||||
[InlineData(CutDirection.TowardOrigin)]
|
||||
public void RegenerateCutOffs_ListOverload_PlacesSegmentsByAscendingPosition(CutDirection direction)
|
||||
{
|
||||
var lower = TestHelpers.MakePartAt(10, 10, 10);
|
||||
var upper = TestHelpers.MakePartAt(10, 30, 10);
|
||||
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings { CutDirection = direction },
|
||||
new Dictionary<CutOff, IReadOnlyList<int>> { [cutOff] = [0, 2, 4] });
|
||||
|
||||
AssertArrangement(plate, cutOff, lower, upper);
|
||||
// The program runs toward the origin, but the file lists segments ascending.
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
Assert.True(segments[0].Start < segments[1].Start && segments[1].Start < segments[2].Start);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_ListOfWrongLength_PlacesSegmentsConsecutively()
|
||||
{
|
||||
var lower = TestHelpers.MakePartAt(10, 10, 10);
|
||||
var upper = TestHelpers.MakePartAt(10, 30, 10);
|
||||
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings(),
|
||||
new Dictionary<CutOff, IReadOnlyList<int>> { [cutOff] = [1, 7] });
|
||||
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
Assert.Equal(new[] { lower, segments[0], segments[1], segments[2], upper }, plate.Parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_IndexOverload_PlacesSegmentsConsecutively()
|
||||
{
|
||||
var lower = TestHelpers.MakePartAt(10, 10, 10);
|
||||
var upper = TestHelpers.MakePartAt(10, 30, 10);
|
||||
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings(), new Dictionary<CutOff, int> { [cutOff] = 1 });
|
||||
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
Assert.Equal(new[] { lower, segments[0], segments[1], segments[2], upper }, plate.Parts);
|
||||
Assert.All(segments, s => Assert.Equal(2, s.Program.Codes.Count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SegmentPrograms_SplitTheLineAtEachPierce()
|
||||
{
|
||||
var (plate, cutOff, _, _) = ThreeSegments();
|
||||
|
||||
var programs = cutOff.SegmentPrograms();
|
||||
|
||||
Assert.Equal(3, programs.Count);
|
||||
Assert.All(programs, p =>
|
||||
{
|
||||
Assert.IsType<RapidMove>(p.Codes[0]);
|
||||
Assert.IsType<LinearMove>(p.Codes[1]);
|
||||
Assert.Equal(2, p.Codes.Count);
|
||||
});
|
||||
Assert.Equal(cutOff.Drawing.Program.Codes, programs.SelectMany(p => p.Codes));
|
||||
Assert.Equal(programs.Count, plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
}
|
||||
|
||||
// Two squares one above the other with a vertical line through both: a segment below,
|
||||
// between and above them.
|
||||
internal static (Plate Plate, CutOff CutOff, Part Lower, Part Upper) ThreeSegments()
|
||||
{
|
||||
var lower = TestHelpers.MakePartAt(10, 10, 10);
|
||||
var upper = TestHelpers.MakePartAt(10, 30, 10);
|
||||
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
Assert.Equal(5, plate.Parts.Count);
|
||||
return (plate, cutOff, lower, upper);
|
||||
}
|
||||
|
||||
private static void AssertArrangement(Plate plate, CutOff cutOff, Part lower, Part upper)
|
||||
{
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
Assert.Equal(3, segments.Count);
|
||||
Assert.Equal(new[] { segments[0].Part, lower, segments[1].Part, upper, segments[2].Part }, plate.Parts);
|
||||
}
|
||||
|
||||
// The clearance outline is offset conservatively, so a trimmed end can sit a few ten
|
||||
// thousandths farther from the part than the nominal clearance.
|
||||
private static void AssertSegment(CutOffSegment segment, double start, double end, double spanStart, double spanEnd)
|
||||
{
|
||||
Assert.Equal(start, segment.Start, 0.001);
|
||||
Assert.Equal(end, segment.End, 0.001);
|
||||
Assert.Equal(spanStart, segment.SpanStart, 0.001);
|
||||
Assert.Equal(spanEnd, segment.SpanEnd, 0.001);
|
||||
Assert.True(segment.Part.BaseDrawing.IsCutOff);
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,10 @@ public class CutOffSeparationTests
|
||||
Assert.False(CutOff.SeparatesSheet(Part(plate, limited), null, plate.BoundingBox(false)));
|
||||
}
|
||||
|
||||
// A line trimmed around a part is two segment parts, neither of which spans the sheet.
|
||||
private static bool Separates(Plate plate, CutOff cutOff) =>
|
||||
CutOff.SeparatesSheet(Part(plate, cutOff), cutOff, plate.BoundingBox(includeParts: false));
|
||||
TestHelpers.CutOffParts(plate, cutOff) is [var part]
|
||||
&& CutOff.SeparatesSheet(part, cutOff, plate.BoundingBox(includeParts: false));
|
||||
|
||||
private static Part Part(Plate plate, CutOff cutOff) =>
|
||||
Assert.Single(plate.Parts, p => ReferenceEquals(p.BaseDrawing, cutOff.Drawing));
|
||||
|
||||
@@ -6,6 +6,46 @@ namespace OpenNest.Tests.CutOffs;
|
||||
|
||||
public class CutOffSerializationTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundTrip_SegmentsKeepTheirOwnPlacesInTheSequence()
|
||||
{
|
||||
var nest = new Nest();
|
||||
var drawing = TestHelpers.MakeSquareDrawing();
|
||||
nest.Drawings.Add(drawing);
|
||||
var lower = new Part(drawing, new Vector(10, 10));
|
||||
var upper = new Part(drawing, new Vector(10, 30));
|
||||
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
|
||||
nest.Plates.Add(plate);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
plate.Parts.Reorder([segments[0], lower, segments[1], upper, segments[2]]);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
Assert.True(new NestWriter(nest).Write(stream));
|
||||
stream.Position = 0;
|
||||
using (var archive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Read, leaveOpen: true))
|
||||
using (var reader = new StreamReader(archive.GetEntry("nest.json")!.Open()))
|
||||
using (var json = System.Text.Json.JsonDocument.Parse(reader.ReadToEnd()))
|
||||
{
|
||||
var saved = Assert.Single(Assert.Single(json.RootElement.GetProperty("plates").EnumerateArray())
|
||||
.GetProperty("cutOffs").EnumerateArray());
|
||||
Assert.Equal(0, saved.GetProperty("sequence").GetInt32());
|
||||
Assert.Equal(new[] { 0, 2, 4 }, saved.GetProperty("segments").EnumerateArray().Select(e => e.GetInt32()));
|
||||
}
|
||||
stream.Position = 0;
|
||||
var loaded = new NestReader(stream).Read();
|
||||
|
||||
var loadedPlate = Assert.Single(loaded.Plates);
|
||||
var loadedCutOff = Assert.Single(loadedPlate.CutOffs);
|
||||
var loadedSegments = CutOffSegments.Of(loadedPlate, loadedCutOff);
|
||||
Assert.Equal(3, loadedSegments.Count);
|
||||
Assert.Equal(new[] { true, false, true, false, true }, loadedPlate.Parts.Select(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.Equal(new[] { 0, 2, 4 }, loadedSegments.Select(s => loadedPlate.Parts.IndexOf(s.Part)));
|
||||
Assert.True(loadedSegments[0].End < loadedSegments[1].Start && loadedSegments[1].End < loadedSegments[2].Start);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_CutOffsPreserved()
|
||||
{
|
||||
|
||||
@@ -146,9 +146,7 @@ public class CutOffVertexParityTests
|
||||
{
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(settings);
|
||||
var cutPart = Assert.Single(plate.Parts.Where(p => p.BaseDrawing.IsCutOff));
|
||||
program = cutPart.Program;
|
||||
location = cutPart.Location;
|
||||
program = TestHelpers.CutOffProgram(plate, cutoff);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Tests.CutOffs;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Cut-off segments are planned one by one: each precedes only the parts in its span of the
|
||||
/// line, the parts are planned as if the segments were not there, and each segment is cut as
|
||||
/// late as it can be before its first part, where its rapids cross nothing cut before it.
|
||||
/// </summary>
|
||||
public class CutOffSegmentPlanningTests
|
||||
{
|
||||
[Fact]
|
||||
public void Capture_GivesEachSegmentThePartsInItsSpan()
|
||||
{
|
||||
var (plate, cutOff, lower, upper) = CutOffSegmentsTests.ThreeSegments();
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => plate.Parts.IndexOf(s.Part)).ToArray();
|
||||
|
||||
var snapshot = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate));
|
||||
|
||||
Assert.Equal(new[] { segments[0], segments[1] }, snapshot.Dependencies.PrerequisitesOf(plate.Parts.IndexOf(lower)));
|
||||
Assert.Equal(new[] { segments[1], segments[2] }, snapshot.Dependencies.PrerequisitesOf(plate.Parts.IndexOf(upper)));
|
||||
Assert.All(segments, s => Assert.Empty(snapshot.Dependencies.PrerequisitesOf(s)));
|
||||
Assert.Equal(segments, CutOffInsertion.Attached(snapshot));
|
||||
}
|
||||
|
||||
// Locked squares in a row and a line through the last one, C. Both segments go straight
|
||||
// before C when the squares before it leave the tool in plain sight of their pierces. A
|
||||
// square D below the row, left from its top-left corner, walls off the bottom segment's
|
||||
// pierce: cut just before C, that segment's approach would cross D, so it goes one gap
|
||||
// earlier, before D, while the top segment (reached over D) stays before C. Segments go
|
||||
// where the route is, never all the way to the start.
|
||||
[Theory]
|
||||
[InlineData(false, "A B s0 s1 C")]
|
||||
[InlineData(true, "A B s0 D s1 C")]
|
||||
public void Plan_PlacesEachSegmentInTheLatestGapWhereItsRapidsAreClear(bool wall, string expected)
|
||||
{
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Size = new Size(60, 120);
|
||||
var a = LockedSquare("A", new Vector(5, 10), topLeft: false);
|
||||
var b = LockedSquare("B", new Vector(25, 10), topLeft: false);
|
||||
var c = LockedSquare("C", new Vector(50, 12), topLeft: false);
|
||||
plate.Parts.Add(a);
|
||||
plate.Parts.Add(b);
|
||||
if (wall)
|
||||
plate.Parts.Add(LockedSquare("D", new Vector(37, 0), topLeft: true));
|
||||
plate.Parts.Add(c);
|
||||
var cutOff = new CutOff(new Vector(55, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
Assert.Equal(2, segments.Count);
|
||||
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed, Describe(result));
|
||||
string Name(Part part) => part.BaseDrawing.IsCutOff
|
||||
? $"s{segments.ToList().FindIndex(s => ReferenceEquals(s.Part, part))}" : part.BaseDrawing.Name;
|
||||
Assert.Equal(expected, string.Join(" ", result.ProposedOrder.Select(o => Name(o.SourcePart))));
|
||||
}
|
||||
|
||||
// Two rows of interlocking parts (each column's neck sits beside its neighbours' bodies)
|
||||
// with lines through two columns and a tail separator. The parts keep the route they get
|
||||
// without the lines; each segment precedes the parts beside it and nothing else.
|
||||
[Fact]
|
||||
public void WalledColumns_PartsKeepTheirRoute_SegmentsPrecedeThePartsBesideThem()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var (plain, _) = WalledColumns(lines: false);
|
||||
var withoutLines = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plain, confirmedParameters: parameters));
|
||||
Assert.True(withoutLines.Status == CuttingPlanStatus.Ready, Describe(withoutLines));
|
||||
|
||||
var (plate, lines) = WalledColumns(lines: true);
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed, Describe(result));
|
||||
var order = result.ProposedOrder.Select(o => o.SourcePart).ToList();
|
||||
Assert.True(order[^1].BaseDrawing.IsCutOff && result.ProposedOrder[^1].SeparatesSheet);
|
||||
Assert.Equal(withoutLines.ProposedOrder.Where(o => !o.IsCutOff).Select(o => o.SourcePart.BoundingBox.Location),
|
||||
order.Where(p => !p.BaseDrawing.IsCutOff).Select(p => p.BoundingBox.Location));
|
||||
var bounds = plate.BoundingBox(includeParts: false);
|
||||
var segments = 0;
|
||||
foreach (var line in lines)
|
||||
foreach (var segment in CutOffSegments.Of(plate, line))
|
||||
{
|
||||
segments++;
|
||||
var beside = plate.Parts.Where(p => !p.BaseDrawing.IsCutOff && CuttingDependencyGraph.CutOffCrosses(
|
||||
line.Axis, line.Position, segment.SpanStart, segment.SpanEnd, p.BoundingBox, bounds)).ToList();
|
||||
Assert.NotEmpty(beside);
|
||||
Assert.All(beside, part => Assert.True(order.IndexOf(segment.Part) < order.IndexOf(part)));
|
||||
// Cut where the route is: never first, with at least one part cut before it.
|
||||
Assert.True(order.IndexOf(segment.Part) > 0);
|
||||
}
|
||||
Assert.Equal(6, segments);
|
||||
|
||||
// Applied and replanned with the order kept, the segments are looked through: the
|
||||
// parts get the entries and routes the plan had, and it replays Ready again.
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
var kept = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters,
|
||||
preservePartOrder: true));
|
||||
Assert.True(kept.Status == CuttingPlanStatus.Ready && kept.IndependentlyReplayed, Describe(kept));
|
||||
Assert.Equal(order, kept.ProposedOrder.Select(o => o.SourcePart));
|
||||
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
||||
}
|
||||
|
||||
private const double Gap = 0.25, Width = 10.6, Neck = 5.6, Height = 20, Margin = 0.25;
|
||||
// The slanted shoulders need more horizontal room than the gap.
|
||||
private const double Pitch = (Width + Neck) / 2 + 2 * Gap;
|
||||
|
||||
private static (Plate Plate, CutOff[] Lines) WalledColumns(bool lines)
|
||||
{
|
||||
const int columns = 8;
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
var sheetX = 2 * Margin + (columns - 1) * Pitch + Width + 4;
|
||||
plate.Size = new Size(2 * Margin + 2 * Height + Gap, sheetX);
|
||||
var upright = new Drawing("upright", Bottle(false));
|
||||
var inverted = new Drawing("inverted", Bottle(true));
|
||||
for (var row = 0; row < 2; row++)
|
||||
for (var column = 0; column < columns; column++)
|
||||
plate.Parts.Add(new Part(column % 2 == 0 ? upright : inverted,
|
||||
new Vector(Margin + column * Pitch, Margin + row * (Height + Gap))));
|
||||
// Beyond the parts: one continuous cut that splits off the tail.
|
||||
var cutOffs = new List<CutOff> { new(new Vector(sheetX - 2, 0), CutOffAxis.Vertical) };
|
||||
if (lines)
|
||||
{
|
||||
cutOffs.Add(new(new Vector(Margin + 2 * Pitch + Width * 0.6, 0), CutOffAxis.Vertical));
|
||||
cutOffs.Add(new(new Vector(Margin + 5 * Pitch + Width * 0.6, 0), CutOffAxis.Vertical));
|
||||
}
|
||||
foreach (var cutOff in cutOffs)
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
PlateSequencing.PlaceCutOffs(plate, cutOffs);
|
||||
return (plate, cutOffs.Skip(1).ToArray());
|
||||
}
|
||||
|
||||
// A narrow neck below a slanted shoulder and a body with chamfered top corners, with two
|
||||
// square holes; inverted, mirrored top to bottom.
|
||||
private static Program Bottle(bool inverted)
|
||||
{
|
||||
const double overhang = (Width - Neck) / 2, chamfer = 2.5;
|
||||
var points = new List<Vector>
|
||||
{
|
||||
new(overhang, 0), new(overhang, Height * 0.4), new(0, Height * 0.6), new(0, Height * 0.85),
|
||||
new(chamfer, Height), new(Width - chamfer, Height), new(Width, Height * 0.85),
|
||||
new(Width, Height * 0.6), new(overhang + Neck, Height * 0.4), new(overhang + Neck, 0),
|
||||
};
|
||||
if (inverted)
|
||||
points = points.Select(p => new Vector(p.X, Height - p.Y)).Reverse().ToList();
|
||||
var program = new Program();
|
||||
program.MoveTo(points[0].X, points[0].Y);
|
||||
foreach (var point in points.Skip(1))
|
||||
program.LineTo(point.X, point.Y);
|
||||
program.LineTo(points[0].X, points[0].Y);
|
||||
foreach (var y in new[] { 2.0, Height - 2.5 })
|
||||
{
|
||||
const double x = Width / 2 - 0.25;
|
||||
program.MoveTo(x, y);
|
||||
program.LineTo(x + 0.5, y);
|
||||
program.LineTo(x + 0.5, y + 0.5);
|
||||
program.LineTo(x, y + 0.5);
|
||||
program.LineTo(x, y);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// A 10 x 10 square cut from (and left at) its bottom-right or top-left corner, with a
|
||||
// short lead-in and lead-out, locked so the planner keeps it as given.
|
||||
private static Part LockedSquare(string name, Vector location, bool topLeft)
|
||||
{
|
||||
var clean = new Program();
|
||||
clean.MoveTo(0, 0);
|
||||
clean.LineTo(0, 10);
|
||||
clean.LineTo(10, 10);
|
||||
clean.LineTo(10, 0);
|
||||
clean.LineTo(0, 0);
|
||||
var part = new Part(new Drawing(name, clean), location);
|
||||
var placed = new Program();
|
||||
var corners = topLeft
|
||||
? new[] { new Vector(0, 10), new Vector(10, 10), new Vector(10, 0), new Vector(0, 0) }
|
||||
: new[] { new Vector(10, 0), new Vector(0, 0), new Vector(0, 10), new Vector(10, 10) };
|
||||
var lead = topLeft ? new Vector(-0.2, 10.2) : new Vector(10.2, -0.2);
|
||||
placed.MoveTo(lead.X, lead.Y);
|
||||
placed.Codes.Add(new LinearMove(corners[0]) { Layer = LayerType.Leadin });
|
||||
foreach (var corner in corners.Skip(1).Append(corners[0]))
|
||||
placed.Codes.Add(new LinearMove(corner));
|
||||
placed.Codes.Add(new LinearMove(lead) { Layer = LayerType.Leadout });
|
||||
Assert.True(part.RestoreLeadInProgram(placed, locked: true));
|
||||
return part;
|
||||
}
|
||||
|
||||
private static string Describe(CuttingPlanResult r) =>
|
||||
$"{r.Status}, expanded {r.Expansions}: " + string.Join("; ", r.Findings.Select(f => f.Message));
|
||||
}
|
||||
@@ -55,8 +55,11 @@ public class CuttingDependencyTests
|
||||
var order = result.ProposedOrder.Select(o => o.SourcePart).ToList();
|
||||
Assert.Same(separator, order[^1]);
|
||||
Assert.True(result.ProposedOrder[^1].SeparatesSheet);
|
||||
Assert.False(result.ProposedOrder.Single(o => ReferenceEquals(o.SourcePart, crossing)).SeparatesSheet);
|
||||
Assert.True(order.IndexOf(crossing) < order.IndexOf(left));
|
||||
foreach (var segment in crossing)
|
||||
{
|
||||
Assert.False(result.ProposedOrder.Single(o => ReferenceEquals(o.SourcePart, segment)).SeparatesSheet);
|
||||
Assert.True(order.IndexOf(segment) < order.IndexOf(left));
|
||||
}
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
Assert.Same(separator, plate.Parts[^1]);
|
||||
Assert.Contains(right, plate.Parts);
|
||||
@@ -111,8 +114,9 @@ public class CuttingDependencyTests
|
||||
}
|
||||
|
||||
// 100 x 100 sheet: squares at X 1-11 and 60-70; a cutoff at X 5 is trimmed around the left
|
||||
// square, while a continuous line at X 40 splits the sheet. Both cutoffs start the sequence.
|
||||
private static (Plate, Part Separator, Part Crossing, Part Left, Part Right) SeparatedPlate()
|
||||
// square into two segment parts, while a continuous line at X 40 splits the sheet. Both
|
||||
// cutoffs start the sequence.
|
||||
private static (Plate, Part Separator, Part[] Crossing, Part Left, Part Right) SeparatedPlate()
|
||||
{
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
@@ -127,11 +131,12 @@ public class CuttingDependencyTests
|
||||
plate.CutOffs.Add(crossing);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
var cuts = plate.Parts.Where(p => p.BaseDrawing.IsCutOff).ToArray();
|
||||
Assert.Equal(2, cuts.Length);
|
||||
Assert.Equal(3, cuts.Length);
|
||||
var separatorPart = cuts.Single(p => ReferenceEquals(p.BaseDrawing, separator.Drawing));
|
||||
var crossingPart = cuts.Single(p => ReferenceEquals(p.BaseDrawing, crossing.Drawing));
|
||||
plate.Parts.Reorder([separatorPart, crossingPart, left, right]);
|
||||
return (plate, separatorPart, crossingPart, left, right);
|
||||
var crossingParts = cuts.Where(p => ReferenceEquals(p.BaseDrawing, crossing.Drawing)).ToArray();
|
||||
Assert.Equal(2, crossingParts.Length);
|
||||
plate.Parts.Reorder([separatorPart, .. crossingParts, left, right]);
|
||||
return (plate, separatorPart, crossingParts, left, right);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
@@ -235,12 +236,15 @@ public class PlateSequencingTests
|
||||
plate.CutOffs.Add(upper);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
Assert.Equal(lower.Drawing.Name, upper.Drawing.Name);
|
||||
var lowerPart = plate.Parts.Single(p => ReferenceEquals(p.BaseDrawing, lower.Drawing));
|
||||
var upperPart = plate.Parts.Single(p => ReferenceEquals(p.BaseDrawing, upper.Drawing));
|
||||
// Each line is trimmed around its part into two segment parts.
|
||||
var lowerParts = TestHelpers.CutOffParts(plate, lower);
|
||||
var upperParts = TestHelpers.CutOffParts(plate, upper);
|
||||
Assert.Equal(2, lowerParts.Length);
|
||||
Assert.Equal(2, upperParts.Length);
|
||||
|
||||
PlateSequencing.Apply(plate, new SequenceParameters { Method = method });
|
||||
|
||||
Assert.Equal(new[] { upperPart, second, lowerPart, first }, plate.Parts);
|
||||
Assert.Equal(upperParts.Append(second).Concat(lowerParts).Append(first), plate.Parts);
|
||||
Assert.Equal(new[] { lower, upper }, plate.CutOffs);
|
||||
}
|
||||
|
||||
@@ -277,7 +281,7 @@ public class PlateSequencingTests
|
||||
|
||||
Assert.Same(separatorPart, plate.Parts[^1]);
|
||||
AssertPrecedes(plate, crossing, right);
|
||||
Assert.Equal(4, plate.Parts.Count);
|
||||
Assert.Equal(5, plate.Parts.Count); // The crossing line is two segment parts.
|
||||
}
|
||||
// Without the rule, at least one route cuts the separator before a part.
|
||||
Assert.True(hazards > 0);
|
||||
@@ -332,7 +336,8 @@ public class PlateSequencingTests
|
||||
PlateSequencing.Apply(plate,
|
||||
new SequenceParameters { Method = SequenceMethod.LeftSide });
|
||||
AssertPrecedes(plate, crossing, part);
|
||||
Assert.False(plate.Parts[1].BaseDrawing.IsCutOff);
|
||||
Assert.Equal(2, TestHelpers.CutOffParts(plate, crossing).Length); // Trimmed around the part.
|
||||
Assert.Same(part, plate.Parts[2]);
|
||||
var expected = plate.Parts.Select(p => p.BaseDrawing.Name).ToArray();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
@@ -401,6 +406,24 @@ public class PlateSequencingTests
|
||||
Assert.Same(square, plate.Parts.First(p => !p.BaseDrawing.IsCutOff));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlaceCutOffs_PlacesEachSegmentBeforeTheFirstPartInItsSpan()
|
||||
{
|
||||
var upper = TestHelpers.MakePartAt(10, 30, 10);
|
||||
var lower = TestHelpers.MakePartAt(10, 10, 10);
|
||||
var plate = TestHelpers.MakePlate(60, 120, upper, lower);
|
||||
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
|
||||
Assert.Equal(new[] { upper, lower, segments[0], segments[1], segments[2] }, plate.Parts);
|
||||
|
||||
PlateSequencing.PlaceCutOffs(plate, [cutOff]);
|
||||
|
||||
// The bottom segment is beside the lower part only; the middle one beside both.
|
||||
Assert.Equal(new[] { segments[1], segments[2], upper, segments[0], lower }, plate.Parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlaceCutOffs_MovesOnlyTheGivenCutOffsWhereTheRulesNeedThem()
|
||||
{
|
||||
@@ -418,30 +441,40 @@ public class PlateSequencingTests
|
||||
// As staging adds them: appended after everything, the existing separator included.
|
||||
plate.CutOffs.AddRange([crossing, limited, separator]);
|
||||
plate.RegenerateCutOffs(settings);
|
||||
Part Cut(CutOff cutOff) => plate.Parts.Single(p => ReferenceEquals(p.BaseDrawing, cutOff.Drawing));
|
||||
Assert.Equal(new[] { a, b, c, Cut(existingSeparator), Cut(crossing), Cut(limited), Cut(separator) },
|
||||
plate.Parts);
|
||||
Part[] Cut(CutOff cutOff) => TestHelpers.CutOffParts(plate, cutOff);
|
||||
Assert.Equal(2, Cut(crossing).Length); // Trimmed around b: two segment parts.
|
||||
Assert.Equal(new[] { a, b, c }.Concat(Cut(existingSeparator)).Concat(Cut(crossing)).Concat(Cut(limited))
|
||||
.Concat(Cut(separator)), plate.Parts);
|
||||
var removed = 0;
|
||||
plate.PartRemoved += (_, e) => Assert.True(e.Item.BaseDrawing.IsCutOff, "Only cut-off parts move.");
|
||||
plate.PartRemoved += (_, _) => removed++;
|
||||
|
||||
PlateSequencing.PlaceCutOffs(plate, [crossing, limited, separator]);
|
||||
|
||||
Assert.Equal(new[] { a, Cut(crossing), b, c, Cut(limited), Cut(existingSeparator), Cut(separator) },
|
||||
plate.Parts);
|
||||
Assert.Equal(3, removed);
|
||||
Assert.Equal(new[] { a }.Concat(Cut(crossing)).Concat([b, c]).Concat(Cut(limited)).Concat(Cut(existingSeparator))
|
||||
.Concat(Cut(separator)), plate.Parts);
|
||||
Assert.Equal(4, removed);
|
||||
}
|
||||
|
||||
private static Part[] Baseline(Plate plate, SequenceParameters parameters) =>
|
||||
PartSequencerFactory.Create(parameters).Sequence(plate.Parts.ToList(), plate)
|
||||
.Select(p => p.Part).Reverse().ToArray();
|
||||
|
||||
// Each of the cut-off's segment parts precedes the crossed parts in its span of the line,
|
||||
// and every crossed part follows at least one segment.
|
||||
private static void AssertPrecedes(Plate plate, CutOff cutOff, params Part[] crossed)
|
||||
{
|
||||
var cutPart = Assert.Single(plate.Parts,
|
||||
p => ReferenceEquals(p.BaseDrawing, cutOff.Drawing));
|
||||
var bounds = plate.BoundingBox(includeParts: false);
|
||||
var segments = CutOffSegments.Of(plate, cutOff);
|
||||
Assert.NotEmpty(segments);
|
||||
foreach (var part in crossed)
|
||||
Assert.True(plate.Parts.IndexOf(cutPart) < plate.Parts.IndexOf(part),
|
||||
$"{cutOff.Drawing.Name} must precede the part at {part.Location}.");
|
||||
{
|
||||
var beside = segments.Where(segment => CuttingDependencyGraph.CutOffCrosses(cutOff.Axis, cutOff.Position,
|
||||
segment.SpanStart, segment.SpanEnd, part.BoundingBox, bounds)).ToList();
|
||||
Assert.NotEmpty(beside);
|
||||
foreach (var segment in beside)
|
||||
Assert.True(plate.Parts.IndexOf(segment.Part) < plate.Parts.IndexOf(part),
|
||||
$"{cutOff.Drawing.Name} [{segment.Start}, {segment.End}] must precede the part at {part.Location}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,25 @@ internal static class TestHelpers
|
||||
return plate;
|
||||
}
|
||||
|
||||
/// <summary>A cut-off's segment parts in cut-sequence order: each trimmed segment is its own part.</summary>
|
||||
public static Part[] CutOffParts(Plate plate, CutOff cutOff) =>
|
||||
plate.Parts.Where(p => ReferenceEquals(p.BaseDrawing, cutOff.Drawing)).ToArray();
|
||||
|
||||
/// <summary>A cut-off's materialized cutting moves: its segment parts' programs joined in cut-sequence order.</summary>
|
||||
public static Program CutOffProgram(Plate plate, CutOff cutOff)
|
||||
{
|
||||
var program = new Program();
|
||||
foreach (var part in CutOffParts(plate, cutOff))
|
||||
{
|
||||
Assert.Equal(Vector.Zero, part.Location);
|
||||
program.Codes.AddRange(part.Program.Codes);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <summary>The number of parts the plate's cut-offs materialize, one per trimmed segment.</summary>
|
||||
public static int CutOffPartCount(Plate plate) => plate.CutOffs.Sum(c => c.SegmentPrograms().Count);
|
||||
|
||||
public static Drawing MakeSquareDrawing(double size = 10)
|
||||
{
|
||||
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
||||
|
||||
@@ -233,7 +233,8 @@ public class AutomaticCutOffFormTests
|
||||
Assert.Same(existing, view.Plate.CutOffs[0]);
|
||||
Assert.Equal(expected.Definitions.Select(c => c.Position.X),
|
||||
view.Plate.CutOffs.Skip(1).Select(c => c.Position.X));
|
||||
Assert.Equal(view.Plate.CutOffs.Count, view.Plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.Equal(view.Plate.CutOffs.Sum(c => c.SegmentPrograms().Count),
|
||||
view.Plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
|
||||
Assert.All(preview, p => Assert.DoesNotContain(p, view.Plate.Parts));
|
||||
Assert.Empty(PreviewParts(view));
|
||||
});
|
||||
|
||||
@@ -272,35 +272,40 @@ namespace OpenNest.Controls
|
||||
if (program == null || program.Codes.Count == 0)
|
||||
continue;
|
||||
|
||||
var activePen = view.Selection.SelectedCutOffs.Contains(cutoff) ? selectedPen
|
||||
: IsCutOffPending(plate, cutoff) ? mutedPen
|
||||
: pen;
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
if (view.Selection.SelectedCutOffs.Contains(cutoff))
|
||||
{
|
||||
if (
|
||||
program.Codes[i] is RapidMove rapid
|
||||
&& program.Codes[i + 1] is LinearMove linear
|
||||
)
|
||||
{
|
||||
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
|
||||
}
|
||||
DrawCutOffSegments(g, program, Vector.Zero, selectedPen);
|
||||
continue;
|
||||
}
|
||||
if (view.CutSimulation.IsActive)
|
||||
{
|
||||
// The simulator mutes parts the torch has not reached yet; each of the
|
||||
// cut-off's trimmed segments is its own part, found by its drawing's reference.
|
||||
for (var i = 0; i < plate.Parts.Count; i++)
|
||||
{
|
||||
var part = plate.Parts[i];
|
||||
if (ReferenceEquals(part.BaseDrawing, cutoff.Drawing))
|
||||
DrawCutOffSegments(g, part.Program, part.Location,
|
||||
view.CutSimulation.IsPartPending(i) ? mutedPen : pen);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
DrawCutOffSegments(g, program, Vector.Zero, pen);
|
||||
}
|
||||
}
|
||||
|
||||
// The simulator mutes parts the torch has not reached yet; a cut-off line is its
|
||||
// part's cutting path, found by its drawing's reference.
|
||||
private bool IsCutOffPending(Plate plate, CutOff cutoff)
|
||||
private void DrawCutOffSegments(Graphics g, Program program, Vector offset, Pen pen)
|
||||
{
|
||||
if (!view.CutSimulation.IsActive)
|
||||
return false;
|
||||
for (var i = 0; i < plate.Parts.Count; i++)
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (ReferenceEquals(plate.Parts[i].BaseDrawing, cutoff.Drawing))
|
||||
return view.CutSimulation.IsPartPending(i);
|
||||
if (
|
||||
program.Codes[i] is RapidMove rapid
|
||||
&& program.Codes[i + 1] is LinearMove linear
|
||||
)
|
||||
{
|
||||
DrawLine(g, rapid.EndPoint + offset, linear.EndPoint + offset, pen);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -34,7 +34,7 @@ Existing full-width vertical cut-offs inside the used span become fixed strip bo
|
||||
|
||||
## Limits and operator review
|
||||
|
||||
**View > Draw Rapids** follows the complete cutting sequence: an incoming rapid ends at a cutoff's first pierce, gaps between its trimmed segments remain rapids, and the next part's rapid starts at the cutoff's final cutting endpoint—not its last pierce. This display does not add a return move or alter the cutoff program.
|
||||
Each trimmed segment of a cut-off is materialized as a part of its own with its own place in the cutting sequence (see [Part sequencing](#part-sequencing)), so one line can be cut in pieces, each where the route passes it. **View > Draw Rapids** follows the complete cutting sequence: an incoming rapid ends at a segment's pierce, and the next part's rapid starts at that segment's cutting endpoint—not its pierce. The cut simulation mutes each segment until the torch reaches it. This display does not add a return move or alter the cutoff program.
|
||||
|
||||
- Maximum strip spacing is not a disconnected-scrap guarantee. Clearance gaps and suppressed short segments can leave bridges between scrap regions. This command does **not** certify that every connected scrap piece is disconnected or fits a hopper.
|
||||
- The active sheet's physical width is used, not a hard-coded 81 inches. Check the actual width against the hopper; a wider sheet is not automatically hopper-compatible.
|
||||
@@ -47,9 +47,9 @@ 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 cutoff is moved earlier as needed so it is cut before every part its nominal line passes through. 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. 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 and its start/end limits, not the trimmed cutting segments, which intentionally skip the 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.
|
||||
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.
|
||||
|
||||
Drawings marked **Cut first** in the drawing editor (for example test squares) are sequenced ahead of every other part: their parts get their own route with the chosen method, then the remaining parts follow on theirs. A cutoff that crosses a cut-first part is still cut before it.
|
||||
|
||||
@@ -59,14 +59,14 @@ The all-plates command uses `PlateSequencing.ApplyAll`, which snapshots the targ
|
||||
|
||||
## Rerunning, editing, and saving
|
||||
|
||||
The command is one-shot; spacing, minimum-strip and minimum-tail inputs start at their defaults each time the dialog opens. Generated lines are ordinary cutoffs and can subsequently be dragged, deleted, or sequenced with the existing tools. Moving parts regenerates their trimmed segments but does not reposition the saved cut-off lines or recalculate the retained tail automatically.
|
||||
The command is one-shot; spacing, minimum-strip and minimum-tail inputs start at their defaults each time the dialog opens. Generated lines are ordinary cutoffs and can subsequently be dragged, deleted, or sequenced with the existing tools; Manual Sequence places each segment on its own. Moving parts regenerates their trimmed segments but does not reposition the saved cut-off lines or recalculate the retained tail automatically. Each regenerated segment goes back to the place in the sequence of the saved segment of its cut-off that it overlaps most along the line; a segment with no saved counterpart (a part dropped onto a segment splits it) follows the segment before it, and a cut-off with no saved segments is added at the end.
|
||||
|
||||
Rerunning unchanged suppresses equivalent existing full-span lines. A limited cutoff at a proposed line is a conflict requiring manual review, not permission to replace it or add overlapping NC moves. Existing definitions are never silently deleted or expanded.
|
||||
|
||||
In the plate view, a left-to-right selection window selects a cut-off only when it encloses every visible cut segment; a right-to-left window selects it when it crosses any visible segment (not an empty gap). Cut-offs and parts can be selected together and deleted together. Clicking a cut-off still starts its existing single-line drag.
|
||||
|
||||
Saving `.nest` retains ordinary cutoff positions, axes, limits, and sequence. Loading regenerates cutoff programs using the reader's default cutoff settings; exact toolpaths made with nondefault cutoff settings are not guaranteed to round-trip. Review regenerated paths before posting.
|
||||
Saving `.nest` retains ordinary cutoff positions, axes, limits, and the sequence place of every segment (see [the file format](nest-file-format.md#cut-offs)). Loading regenerates cutoff programs using the reader's default cutoff settings; exact toolpaths made with nondefault cutoff settings are not guaranteed to round-trip, and a cut-off that regenerates into a different number of segments loads them consecutively from its first saved place. Review regenerated paths before posting.
|
||||
|
||||
## Implementation boundary
|
||||
|
||||
`AutomaticCutOffPlanner` in Core builds detached definitions and diagnostics without changing the input plate. `AutomaticCutOffOptions.MinimumTailLength` is a finite, nonnegative distance in model units; its programmatic default of zero preserves the previous behavior, while the desktop explicitly supplies its unit-aware 12-in / 304.8-mm default. `AutomaticCutOffBatch.Create` plans an ordered plate list, and `Apply` replans the complete list before mutating any plate, refusing a blocked batch and recovering touched cutoff state on exceptions. Callers must prevent concurrent edits throughout planning/apply. The desktop dialog renders the active plate's detached preview through `PlateView.SetActiveParts`; both menu commands apply through the shared batch service, which adds definitions to `Plate.CutOffs` and uses `Plate.RegenerateCutOffs`. It must not accept preview parts directly, because doing so would bypass cutoff persistence and regeneration. There are no new nesting, post-processing, or `.nest` format rules.
|
||||
`AutomaticCutOffPlanner` in Core builds detached definitions and diagnostics without changing the input plate. `AutomaticCutOffOptions.MinimumTailLength` is a finite, nonnegative distance in model units; its programmatic default of zero preserves the previous behavior, while the desktop explicitly supplies its unit-aware 12-in / 304.8-mm default. `AutomaticCutOffBatch.Create` plans an ordered plate list, and `Apply` replans the complete list before mutating any plate, refusing a blocked batch and recovering touched cutoff state on exceptions. Callers must prevent concurrent edits throughout planning/apply. The desktop dialog renders the active plate's detached preview through `PlateView.SetActiveParts`; both menu commands apply through the shared batch service, which adds definitions to `Plate.CutOffs` and uses `Plate.RegenerateCutOffs`. It must not accept preview parts directly, because doing so would bypass cutoff persistence and regeneration. `RegenerateCutOffs` materializes one part per trimmed segment of each definition (`CutOff.SegmentPrograms`), each with its own place in `Plate.Parts`; `CutOffSegments.Of` finds a definition's segment parts and the span of the nominal line each stands for. Posts emit each segment part like any other part. There are no new nesting rules; the `.nest` format records each segment's sequence place.
|
||||
+43
-9
@@ -107,10 +107,13 @@ The overlap analyzer trims the same shape from its own copy (see
|
||||
Capture builds whole-part prerequisites from owned values, and both the search and
|
||||
the final replay enforce them:
|
||||
|
||||
- A cutoff precedes every part its nominal line crosses within its span, using the
|
||||
same rule as automatic sequencing: nominal position and limits against the part's
|
||||
placed bounds, matched by drawing reference, never by name or trimmed segments. A
|
||||
cutoff whose definition is missing precedes every part. Cutoffs need a
|
||||
- A cutoff's trimmed segments are separate parts (see [automatic scrap
|
||||
cutoffs](automatic-scrap-cutoffs.md#part-sequencing)); each precedes every part its
|
||||
nominal line crosses within the segment's span of the line (from the end of the
|
||||
segment before it to the start of the one after it, or the line's limits), using the
|
||||
same rule as automatic sequencing: nominal position and span against the part's
|
||||
placed bounds, matched by drawing reference, never by name or the trimmed cut
|
||||
itself. A cutoff whose definition is missing precedes every part. Cutoffs need a
|
||||
plate-scoped request; they are always fixed programs, need no lead-in, are not
|
||||
material for lead validation and, being open cuts, never become rapid obstacles.
|
||||
Rapids into and out of them are still checked.
|
||||
@@ -174,7 +177,11 @@ state is memoized only in recovery; it never prunes the retained full search.
|
||||
Recovered whole programs are checked again and independently replayed. Single-part
|
||||
and no-hole requests keep their existing search order.
|
||||
|
||||
A preserved order is followed as given. Otherwise the order is an open
|
||||
A preserved order is followed as given, cutoff segments included. Otherwise the
|
||||
cutoff segments that must precede some part are left out of the search: the parts are
|
||||
planned as if the segments were not there, so a plate's route does not change because
|
||||
strip lines were added, and the segments are placed into that plan afterwards (see
|
||||
[cutoff segments](#cutoff-segments) below). The order is an open
|
||||
travelling-salesman path over part centres from the start point: nearest neighbour,
|
||||
then 2-opt reversals and Or-opt moves of one to three parts, never placing a part
|
||||
before a cutoff or nested-part prerequisite. A part with other parts nested in its
|
||||
@@ -217,6 +224,32 @@ search. Recovery shares the original total expansion budget; exhausting it still
|
||||
returns a refusal from the service, never a best-effort upgrade. Only the batch then
|
||||
looks for the separate [safest route](#safest-route-fallback-when-the-search-runs-out).
|
||||
|
||||
### Cutoff segments
|
||||
|
||||
A cutoff segment is a fixed open cut that is cut the same way wherever it goes and is
|
||||
never a rapid obstacle, so all that its place in the order changes are the rapid into
|
||||
its pierce and the rapid from its end to what follows. Once the parts are planned, each
|
||||
segment that must precede some part (`CutOffInsertion`) goes as late as it can before
|
||||
the first part in its span of the line, into the latest gap where both rapids cross
|
||||
nothing cut before that point: the part after it keeps the program it was planned with,
|
||||
and only its incoming rapid is checked again from the segment's end. Segments due in the
|
||||
same gap are chained in the order placed, latest-due first. This keeps cutting local:
|
||||
the segment below a row of parts is cut while the route passes it, not up front, and
|
||||
never before a part it need not precede unless the geometry allows nothing later (a
|
||||
segment whose pierce is walled off by every part cut after a point goes before that
|
||||
point). Each gap tried is charged to the expansion budget. When a segment fits in no
|
||||
gap before its first part, the whole plate is searched again with the segments among
|
||||
the parts, within the same budget, as plates with cutoffs were planned before. A sheet
|
||||
separator or a line through open scrap precedes nothing and is planned like a part: a
|
||||
separator after everything, the other at its tour position.
|
||||
|
||||
Wherever a segment sits in an order, the search looks through it: the part before it
|
||||
faces the next part, not the segment, and the part after it ranks its entries and hole
|
||||
route from where the tool was before the segment, while every rapid is still checked
|
||||
from the segment's end. A kept order with segments where a plan put them (an applied
|
||||
plan replanned with `Keep the current part order`) therefore gets the entries and
|
||||
routes the plan had, and replays Ready again.
|
||||
|
||||
Candidates use native closest points, vertices, midpoints and circle angles in
|
||||
stable order. `maxEntries` caps the normal ranked shortlist, not the recovery
|
||||
catalogue: recovery may inspect the remaining finite native entries, with every
|
||||
@@ -579,10 +612,11 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
|
||||
- `Add scrap cut-offs` runs the automatic cut-off planner (defaults 35 in /
|
||||
889 mm strips, half-strip minimum, 12 in / 304.8 mm tail) on every plate in scope
|
||||
*before* capture, so the cutting plan sequences the new lines like any
|
||||
other cut-off. Each added cut-off is moved in the part list to just before the first part
|
||||
its nominal line crosses (a sheet-separating one last, one that crosses nothing ahead of any
|
||||
existing separator), so a kept order, including the fallback after a free search runs out of
|
||||
budget, satisfies the cut-off rules; other parts and cut-offs keep their positions.
|
||||
other cut-off. Each segment of an added cut-off is moved in the part list to just before the
|
||||
first part in its span of the line (a sheet-separating one last, one that crosses nothing
|
||||
ahead of any existing separator), so a kept order, including the fallback after a free
|
||||
search runs out of budget, satisfies the cut-off rules; other parts and cut-offs keep their
|
||||
positions. The free search places the segments itself (see [cutoff segments](#cutoff-segments)).
|
||||
These are the only live changes made before Apply: Apply keeps them,
|
||||
while unticking the box, Close or switching to Validate removes them and
|
||||
restores the earlier cut-offs and sequence. The proposal preview draws planned cut-offs
|
||||
|
||||
@@ -62,6 +62,22 @@ is raised. The drawing hash is checked against the file text before this
|
||||
repair, so saved programs still load; the next save writes the marks into
|
||||
`programs/program-N`.
|
||||
|
||||
## Cut-offs
|
||||
|
||||
Each serialized plate's `cutOffs` array holds its cut-off definitions: `x`, `y`,
|
||||
`axis` (`vertical` or `horizontal`), optional `startLimit` and `endLimit`, and the
|
||||
places in the plate's cut sequence (`Plate.Parts` order, cut-offs included) of the
|
||||
parts the definition materializes. Loading regenerates each definition with the
|
||||
reader's default cut-off settings and makes one part per trimmed segment. `sequence`
|
||||
is the zero-based place of the first segment (by position along the line); null or
|
||||
absent in older files, which load the cut-off at the end. `segments` lists the place
|
||||
of every segment in ascending order along the line and is written only when the
|
||||
segments are not consecutive, as after Plan Cutting cuts a strip line in pieces where
|
||||
the route passes them. Readers without `segments` place the segments consecutively
|
||||
from `sequence`, which is still a dependency-safe order. A `segments` list whose length
|
||||
differs from the regenerated segment count (nondefault cut-off settings, a moved part)
|
||||
places the segments consecutively from its smallest entry.
|
||||
|
||||
## Plate cutting parameters
|
||||
|
||||
Each plate's optional `cuttingParameters` stores the settings used by Assign
|
||||
|
||||
Reference in new issue
Block a user