fix(core): keep cut-offs in their cut sequence through regenerate and save
A cut-off's place in Plate.Parts is its cut sequence number, but RegenerateCutOffs removed every cut-off part and appended it again, so any part drag, fill or cut-off move sent the cut-offs to the end. The nest file didn't store the position either, so reopening did the same. RegenerateCutOffs now puts each cut-off back at its previous index (new cut-offs go at the end), and CutOffDto.Sequence saves the index. Older files without it load the cut-offs at the end, as before.
This commit is contained in:
@@ -154,7 +154,7 @@ Keep vendor programming manuals and full-text extracts outside source control un
|
||||
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies. After its null/empty guards, `DefaultFillComparer` decides unequal counts without scoring; equal counts still use scores, and exact ties retain the current layout. `FillHelpers.FillPattern` computes eager scores only when no custom comparer is supplied; custom comparers remain authoritative and may perform their own scoring.
|
||||
- **Extents column pitch**: for finite valid geometry, finite pair height, and finite nonnegative spacing, `FillExtents.BuildColumn` uses `pair.Bbox.Width + partSpacing` directly. The old vertical slide calculation clamps to the same pitch, so it need not prepare boundaries or temporary test clones. Negative/nonfinite spacing or nonfinite pair height retains the legacy calculation: public/interactive callers do not all validate spacing. Do not remove `BuildPair` boundary preparation or the adjusted-column overlap fallback, or turn this shortcut into a geometry/validation policy change.
|
||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds each at its previous index in `Plate.Parts` (its cut sequence number; new cut-offs go at the end). Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load; `CutOffDto.Sequence` restores each one's place in the cut sequence). Posts must follow `Plate.Parts` order for cut-offs too, not move them to the end.
|
||||
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). MCP, API, and Training projects use `ImportDrawing` for headless conversion. The console uses `Import` followed by `BuildDrawing` so it can report bend-repair outcomes. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
|
||||
- **GravographIS engrave/cut passes**: The `OpenNest.Posts.GravographIS` post splits geometry by `LayerType` into ordered tool passes — engrave (`Scribe`) then cut (`Cut`/`Leadin`/`Leadout`); `Display` is skipped. `ConvertGeometry` tags DXF layers `ENGRAVE`/`ETCH` and the saved `SCRIBE` layer (lines, arcs, circles) as `Scribe`; the layer round-trips through `.nest` via `NestWriter`/`ProgramReader`. `NestPolylineExtractor.ExtractLayered` carries `LayerType` per polyline (splitting a continuous chain at any layer change); `GravographISPostProcessor.BuildPasses` groups them and `GravographISWriter.Write(IReadOnlyList<GravographPass>, …)` emits each pass at its own feed/depth, parking to origin and emitting an operator pause (motor off → aux off → `LB` console message → motor on) before any pass whose config has `PauseBefore`. Per-pass parameters live in `GravographISPostConfig` (an `IConfigurablePostProcessor` config with `Engrave`/`Cut` `LayerCutConfig` blocks), edited in the shared `PostProcessorConfigForm` PropertyGrid and persisted to JSON. The cut block pauses by default so the operator can swap/adjust the tool (the spring-floated spindle means programmed `DZ` depth is not the real cut depth).
|
||||
|
||||
+34
-3
@@ -94,18 +94,40 @@ 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 added back if it produces any geometry.
|
||||
/// regenerated and put back at the same place in the cut sequence
|
||||
/// (<see cref="Parts"/> order). New cut-offs are added at the end.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings)
|
||||
{
|
||||
// Remove existing cut-off parts
|
||||
// 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>();
|
||||
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
if (!Parts[i].BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
var cutoff = CutOffs.FirstOrDefault(c => ReferenceEquals(c.Drawing, Parts[i].BaseDrawing));
|
||||
if (cutoff != null)
|
||||
sequence[cutoff] = i;
|
||||
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
RegenerateCutOffs(settings, sequence);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary<CutOff, int> sequence)
|
||||
{
|
||||
var cache = BuildPerimeterCache(this);
|
||||
var placed = new List<(int Index, Part Part)>();
|
||||
|
||||
// Regenerate and materialize each cut-off
|
||||
foreach (var cutoff in CutOffs)
|
||||
@@ -116,8 +138,17 @@ namespace OpenNest
|
||||
continue;
|
||||
|
||||
var part = new Part(cutoff.Drawing);
|
||||
|
||||
if (sequence != null && sequence.TryGetValue(cutoff, out var index))
|
||||
placed.Add((index, part));
|
||||
else
|
||||
Parts.Add(part);
|
||||
}
|
||||
|
||||
// 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))
|
||||
Parts.Insert(System.Math.Clamp(index, 0, Parts.Count), part);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -87,6 +87,12 @@ namespace OpenNest.IO
|
||||
public string Axis { get; init; } = "vertical";
|
||||
public double? StartLimit { get; init; }
|
||||
public double? EndLimit { get; init; }
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public int? Sequence { get; init; }
|
||||
}
|
||||
|
||||
public record SizeDto
|
||||
|
||||
@@ -372,6 +372,8 @@ namespace OpenNest.IO
|
||||
// Cut-offs
|
||||
if (p.CutOffs != null)
|
||||
{
|
||||
var sequence = new Dictionary<CutOff, int>();
|
||||
|
||||
foreach (var cutoffDto in p.CutOffs)
|
||||
{
|
||||
var axis =
|
||||
@@ -384,9 +386,12 @@ namespace OpenNest.IO
|
||||
EndLimit = cutoffDto.EndLimit,
|
||||
};
|
||||
plate.CutOffs.Add(cutoff);
|
||||
|
||||
if (cutoffDto.Sequence is int index)
|
||||
sequence[cutoff] = index;
|
||||
}
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
plate.RegenerateCutOffs(new CutOffSettings(), sequence);
|
||||
}
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
@@ -223,6 +223,16 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
cutoffs.Add(
|
||||
new CutOffDto
|
||||
{
|
||||
@@ -231,6 +241,7 @@ namespace OpenNest.IO
|
||||
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
|
||||
StartLimit = cutoff.StartLimit,
|
||||
EndLimit = cutoff.EndLimit,
|
||||
Sequence = sequence >= 0 ? sequence : null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.CutOffs;
|
||||
|
||||
/// <summary>
|
||||
/// A cut-off's place in <c>Plate.Parts</c> is its cut sequence number, which the
|
||||
/// user sets and the posts follow. Regenerating cut-offs (after a part drag, fill
|
||||
/// or cut-off move) and saving must keep it.
|
||||
/// </summary>
|
||||
public class CutOffSequenceTests
|
||||
{
|
||||
private static Drawing MakeSquare()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("square", pgm);
|
||||
}
|
||||
|
||||
/// <summary>Three parts along X with vertical cut-offs between them, the
|
||||
/// cut-offs sequenced as 2 and 4: part, cut, part, cut, part.</summary>
|
||||
private static Plate MakeSequencedPlate(Drawing drawing)
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(1, 2)));
|
||||
plate.Parts.Add(new Part(drawing, new Vector(30, 2)));
|
||||
plate.Parts.Add(new Part(drawing, new Vector(60, 2)));
|
||||
|
||||
plate.CutOffs.Add(new CutOff(new Vector(20, 0), CutOffAxis.Vertical));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(50, 0), CutOffAxis.Vertical));
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
// As ActionSetSequence does: remove, then insert at the chosen number.
|
||||
SetSequence(plate, plate.CutOffs[0], 1);
|
||||
SetSequence(plate, plate.CutOffs[1], 3);
|
||||
return plate;
|
||||
}
|
||||
|
||||
private static void SetSequence(Plate plate, CutOff cutOff, int index)
|
||||
{
|
||||
var part = plate.Parts.First(p => ReferenceEquals(p.BaseDrawing, cutOff.Drawing));
|
||||
plate.Parts.Remove(part);
|
||||
plate.Parts.Insert(index, part);
|
||||
}
|
||||
|
||||
/// <summary>The cut sequence as names: "part" or the cut-off's X position.</summary>
|
||||
private static string[] Sequence(Plate plate) =>
|
||||
plate
|
||||
.Parts.Select(p =>
|
||||
p.BaseDrawing.IsCutOff ? $"cut@{p.BoundingBox.X:F0}" : $"part@{p.Location.X:F0}"
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
private static readonly string[] Expected =
|
||||
{
|
||||
"part@1",
|
||||
"cut@20",
|
||||
"part@30",
|
||||
"cut@50",
|
||||
"part@60",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_KeepsEachCutOffInItsSequencePlace()
|
||||
{
|
||||
var plate = MakeSequencedPlate(MakeSquare());
|
||||
Assert.Equal(Expected, Sequence(plate));
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
Assert.Equal(Expected, Sequence(plate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_AddsNewCutOffAtTheEnd()
|
||||
{
|
||||
var plate = MakeSequencedPlate(MakeSquare());
|
||||
|
||||
plate.CutOffs.Add(new CutOff(new Vector(0, 40), CutOffAxis.Horizontal));
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
Assert.Equal(Expected, Sequence(plate).Take(5));
|
||||
Assert.Equal(6, plate.Parts.Count);
|
||||
Assert.Same(plate.CutOffs[2].Drawing, plate.Parts[5].BaseDrawing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveAndReopen_KeepsCutOffSequence()
|
||||
{
|
||||
var drawing = MakeSquare();
|
||||
var nest = new Nest("seq") { DateCreated = DateTime.Now, DateLastModified = DateTime.Now };
|
||||
nest.Drawings.Add(drawing);
|
||||
nest.Plates.Add(MakeSequencedPlate(drawing));
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest).Write(stream);
|
||||
stream.Position = 0;
|
||||
var loaded = new NestReader(stream).Read();
|
||||
|
||||
Assert.Equal(Expected, Sequence(loaded.Plates[0]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_OutOfRangeSequence_AppendsInsteadOfThrowing()
|
||||
{
|
||||
// A damaged file can name a sequence past the end of the plate.
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(new Part(MakeSquare(), new Vector(1, 2)));
|
||||
var cutOff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutOff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings(), new Dictionary<CutOff, int> { [cutOff] = 99 });
|
||||
|
||||
Assert.Equal(2, plate.Parts.Count);
|
||||
Assert.Same(cutOff.Drawing, plate.Parts[1].BaseDrawing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user