mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 00:21:06 -04:00
fix(nest): anchor placed parts when an edit moves origin
Converter edits rebuild a drawing's program around its first pierce, so Source.Offset can move (legacy drawings with no source entities, or a tabbed/hidden contour changing the first cut). Part.Update kept the old Location, shifting placed parts by the rotated origin delta - about 43 inches on a 270-degree part in the reported nest. Offset rebuilt parts by that delta so their geometry stays put on the plate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6c87df372
commit
0baa737162
2 files changed
+147
-9
No files matched your search
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
@@ -10,11 +11,11 @@ namespace OpenNest;
|
||||
/// </summary>
|
||||
public sealed class DrawingProgramSnapshot
|
||||
{
|
||||
private readonly Dictionary<Drawing, string> programs;
|
||||
private readonly Dictionary<Drawing, Captured> programs;
|
||||
private readonly Func<Program, string> fingerprint;
|
||||
|
||||
private DrawingProgramSnapshot(
|
||||
Dictionary<Drawing, string> programs,
|
||||
Dictionary<Drawing, Captured> programs,
|
||||
Func<Program, string> fingerprint)
|
||||
{
|
||||
this.programs = programs;
|
||||
@@ -32,12 +33,12 @@ public sealed class DrawingProgramSnapshot
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawings);
|
||||
ArgumentNullException.ThrowIfNull(fingerprint);
|
||||
var programs = new Dictionary<Drawing, string>(ReferenceEqualityComparer.Instance);
|
||||
var programs = new Dictionary<Drawing, Captured>(ReferenceEqualityComparer.Instance);
|
||||
|
||||
foreach (var drawing in drawings)
|
||||
{
|
||||
if (!drawing.IsCutOff && !programs.ContainsKey(drawing))
|
||||
programs.Add(drawing, fingerprint(drawing.Program));
|
||||
programs.Add(drawing, new Captured(fingerprint(drawing.Program), SourceOffset(drawing)));
|
||||
}
|
||||
|
||||
return new DrawingProgramSnapshot(programs, fingerprint);
|
||||
@@ -45,20 +46,22 @@ public sealed class DrawingProgramSnapshot
|
||||
|
||||
/// <summary>
|
||||
/// Complete the captured edit by rebuilding changed drawings' parts across all plates.
|
||||
/// Part.Update preserves the placement and clears obsolete lead-ins, tabs and locks.
|
||||
/// Part.Update clears obsolete lead-ins, tabs and locks; the placement is anchored to the
|
||||
/// source frame, so a rebuild that re-origins the program (Source.Offset moved, e.g. a new
|
||||
/// first pierce) leaves the geometry where it was on the plate.
|
||||
/// Unchanged and uncaptured drawings' parts retain their program instances and state.
|
||||
/// Returns the rebuilt parts so a UI can invalidate just their graphics.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Part> UpdateChangedParts(IEnumerable<Plate> plates)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plates);
|
||||
var changed = new HashSet<Drawing>(ReferenceEqualityComparer.Instance);
|
||||
var originShifts = new Dictionary<Drawing, Vector>(ReferenceEqualityComparer.Instance);
|
||||
|
||||
foreach (var entry in programs)
|
||||
{
|
||||
if (!entry.Key.IsCutOff
|
||||
&& !string.Equals(entry.Value, fingerprint(entry.Key.Program), StringComparison.Ordinal))
|
||||
changed.Add(entry.Key);
|
||||
&& !string.Equals(entry.Value.Fingerprint, fingerprint(entry.Key.Program), StringComparison.Ordinal))
|
||||
originShifts.Add(entry.Key, SourceOffset(entry.Key) - entry.Value.SourceOffset);
|
||||
}
|
||||
|
||||
var updated = new List<Part>();
|
||||
@@ -66,14 +69,22 @@ public sealed class DrawingProgramSnapshot
|
||||
{
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
if (!changed.Contains(part.BaseDrawing))
|
||||
if (!originShifts.TryGetValue(part.BaseDrawing, out var shift))
|
||||
continue;
|
||||
|
||||
// A source point P sat at Location + R(P - old); keep it there with the new origin.
|
||||
var rotation = part.Rotation;
|
||||
part.Update();
|
||||
if (shift != Vector.Zero)
|
||||
part.Offset(shift.Rotate(rotation));
|
||||
updated.Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private static Vector SourceOffset(Drawing drawing) => drawing.Source?.Offset ?? Vector.Zero;
|
||||
|
||||
private readonly record struct Captured(string Fingerprint, Vector SourceOffset);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.TabbedCutouts;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Editing placed drawings in the converter rebuilds their programs around a new origin (the first
|
||||
/// pierce). Placed parts must stay where they were on the plate, whatever the edit did to that origin.
|
||||
/// </summary>
|
||||
public class ConverterEditPlacementTests
|
||||
{
|
||||
[Fact]
|
||||
public void TabbingAHoleInALegacyDrawing_KeepsPlacedPartsInPlace()
|
||||
{
|
||||
// Like drawings converted from CNC programs: no source entities, no source offset,
|
||||
// and a program origin away from every pierce.
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(new Vector(-1.049212, -43.0103)));
|
||||
program.Codes.Add(new ArcMove(new Vector(-1.049212, -43.0103), new Vector(-1.049212, -42.7603)));
|
||||
program.Codes.Add(new RapidMove(new Vector(3.555639, -20.6378)));
|
||||
program.Codes.Add(new ArcMove(new Vector(3.555639, -20.6378), new Vector(2.276111, -20.6378)));
|
||||
program.Codes.Add(new RapidMove(new Vector(6.503735, -15.1378)));
|
||||
program.Codes.Add(new LinearMove(new Vector(6.503735, -67.5753)));
|
||||
program.Codes.Add(new LinearMove(new Vector(-1.946512, -67.5753)));
|
||||
program.Codes.Add(new LinearMove(new Vector(-1.946512, -15.1378)));
|
||||
program.Codes.Add(new LinearMove(new Vector(6.503735, -15.1378)));
|
||||
var drawing = new Drawing("4980 A80-10 PT01", program);
|
||||
Assert.Equal(Vector.Zero, drawing.Source.Offset);
|
||||
|
||||
var parts = new[]
|
||||
{
|
||||
Place(drawing, 3 * Angle.HalfPI, new Vector(52.6875, 35.9414)),
|
||||
Place(drawing, Angle.ToRadians(37), new Vector(20, 10)),
|
||||
};
|
||||
|
||||
ConverterEdit(drawing, entities =>
|
||||
{
|
||||
var hole = entities.OfType<Circle>().Single(c => c.Radius.IsEqualTo(1.279528));
|
||||
return new TabbedCutout { EdgeIds = [hole.Id] };
|
||||
}, parts, out var tabbed);
|
||||
|
||||
Assert.Single(tabbed.TabbedCutouts);
|
||||
Assert.NotEqual(Vector.Zero, tabbed.Source.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HidingTheFirstCutHole_KeepsPlacedPartsInPlace()
|
||||
{
|
||||
var firstHole = new Circle(29, 15, 0.5);
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(20, 10, 32, 10),
|
||||
new Line(32, 10, 32, 18),
|
||||
new Line(32, 18, 20, 18),
|
||||
new Line(20, 18, 20, 10),
|
||||
firstHole,
|
||||
new Circle(23, 13, 1),
|
||||
};
|
||||
var result = new CadImportResult { Entities = entities, Name = "Plate", SourcePath = "plate.dxf" };
|
||||
var drawing = CadImporter.BuildDrawing(result, entities, result.Bends, 1, null, null);
|
||||
var before = drawing.Source.Offset;
|
||||
var parts = new[]
|
||||
{
|
||||
Place(drawing, 0, new Vector(5, 5)),
|
||||
Place(drawing, Angle.HalfPI, new Vector(40, 5)),
|
||||
Place(drawing, Angle.ToRadians(37), new Vector(70, 20)),
|
||||
};
|
||||
|
||||
ConverterEdit(drawing, editable =>
|
||||
{
|
||||
editable.Single(e => e.Id == firstHole.Id).IsVisible = false;
|
||||
return null;
|
||||
}, parts, out var edited);
|
||||
|
||||
Assert.NotEqual(before, edited.Source.Offset);
|
||||
}
|
||||
|
||||
private static Part Place(Drawing drawing, double rotation, Vector location)
|
||||
{
|
||||
var part = new Part(drawing);
|
||||
part.Rotate(rotation);
|
||||
part.Location = location;
|
||||
return part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the desktop's converter round trip on <paramref name="drawing"/> (snapshot, editable
|
||||
/// entities, rebuild, in-place apply, part update) and asserts every part's placed bounds held.
|
||||
/// </summary>
|
||||
private static void ConverterEdit(
|
||||
Drawing drawing,
|
||||
Func<List<Entity>, TabbedCutout> edit,
|
||||
Part[] parts,
|
||||
out Drawing edited)
|
||||
{
|
||||
var plate = new Plate(120, 60);
|
||||
foreach (var part in parts)
|
||||
plate.Parts.Add(part);
|
||||
var expected = parts.Select(p => p.BoundingBox).ToArray();
|
||||
var snapshot = DrawingProgramSnapshot.Capture(
|
||||
new[] { drawing },
|
||||
program => NestWriter.GetProgramText(program) + "\0" + NestWriter.GetSubProgramsText(program));
|
||||
|
||||
var editable = CadImporter.EditableEntities(drawing);
|
||||
var cutout = edit(editable);
|
||||
// Derived program entities sit on SpecialLayers.Cut, whose visibility the converter's layer list owns.
|
||||
var visible = editable.Where(e => e.IsVisible).ToList();
|
||||
edited = CadImporter.BuildDrawing(
|
||||
new CadImportResult { Entities = editable, Name = drawing.Name }, visible,
|
||||
new List<OpenNest.Bending.Bend>(), 1, null, null, null,
|
||||
cutout == null ? null : [cutout]);
|
||||
CadImporter.ApplyEdit(drawing, edited);
|
||||
|
||||
Assert.Equal(parts, snapshot.UpdateChangedParts(new[] { plate }));
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var actual = parts[i].BoundingBox;
|
||||
Assert.Equal(expected[i].X, actual.X, 6);
|
||||
Assert.Equal(expected[i].Y, actual.Y, 6);
|
||||
Assert.Equal(expected[i].Length, actual.Length, 6);
|
||||
Assert.Equal(expected[i].Width, actual.Width, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user