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.
|
- `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.
|
- `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.
|
- **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.
|
- **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.
|
- **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).
|
- **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).
|
||||||
|
|||||||
+36
-5
@@ -94,18 +94,40 @@ namespace OpenNest
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regenerates all cut-off drawings and materializes them as parts.
|
/// Regenerates all cut-off drawings and materializes them as parts.
|
||||||
/// Existing cut-off parts are removed first, then each cut-off is
|
/// 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>
|
/// </summary>
|
||||||
public void RegenerateCutOffs(CutOffSettings settings)
|
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--)
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (Parts[i].BaseDrawing.IsCutOff)
|
if (!Parts[i].BaseDrawing.IsCutOff)
|
||||||
Parts.RemoveAt(i);
|
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 cache = BuildPerimeterCache(this);
|
||||||
|
var placed = new List<(int Index, Part Part)>();
|
||||||
|
|
||||||
// Regenerate and materialize each cut-off
|
// Regenerate and materialize each cut-off
|
||||||
foreach (var cutoff in CutOffs)
|
foreach (var cutoff in CutOffs)
|
||||||
@@ -116,8 +138,17 @@ namespace OpenNest
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var part = new Part(cutoff.Drawing);
|
var part = new Part(cutoff.Drawing);
|
||||||
Parts.Add(part);
|
|
||||||
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ namespace OpenNest.IO
|
|||||||
public string Axis { get; init; } = "vertical";
|
public string Axis { get; init; } = "vertical";
|
||||||
public double? StartLimit { get; init; }
|
public double? StartLimit { get; init; }
|
||||||
public double? EndLimit { 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
|
public record SizeDto
|
||||||
|
|||||||
@@ -372,6 +372,8 @@ namespace OpenNest.IO
|
|||||||
// Cut-offs
|
// Cut-offs
|
||||||
if (p.CutOffs != null)
|
if (p.CutOffs != null)
|
||||||
{
|
{
|
||||||
|
var sequence = new Dictionary<CutOff, int>();
|
||||||
|
|
||||||
foreach (var cutoffDto in p.CutOffs)
|
foreach (var cutoffDto in p.CutOffs)
|
||||||
{
|
{
|
||||||
var axis =
|
var axis =
|
||||||
@@ -384,9 +386,12 @@ namespace OpenNest.IO
|
|||||||
EndLimit = cutoffDto.EndLimit,
|
EndLimit = cutoffDto.EndLimit,
|
||||||
};
|
};
|
||||||
plate.CutOffs.Add(cutoff);
|
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);
|
nest.Plates.Add(plate);
|
||||||
|
|||||||
@@ -223,6 +223,16 @@ namespace OpenNest.IO
|
|||||||
var cutoffs = new List<CutOffDto>();
|
var cutoffs = new List<CutOffDto>();
|
||||||
foreach (var cutoff in plate.CutOffs)
|
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(
|
cutoffs.Add(
|
||||||
new CutOffDto
|
new CutOffDto
|
||||||
{
|
{
|
||||||
@@ -231,6 +241,7 @@ namespace OpenNest.IO
|
|||||||
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
|
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
|
||||||
StartLimit = cutoff.StartLimit,
|
StartLimit = cutoff.StartLimit,
|
||||||
EndLimit = cutoff.EndLimit,
|
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