mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 15:51:42 -05:00
feat(cad-converter): show the source file's raw geometry as Original
Show Original drew nothing until Simplify ran, and then showed entities that had already been through import cleanup. The importers now capture the entities before any optimization and the overlay draws those from load, while Simplify keeps starting from the cleaned entities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8076ddbd78
commit
bbdd51fabc
10 files changed
+307
-11
No files matched your search
@@ -18,6 +18,13 @@ namespace OpenNest.IO
|
||||
/// </summary>
|
||||
public List<Entity> Entities { get; set; } = new List<Entity>();
|
||||
|
||||
/// <summary>
|
||||
/// The source file's geometry on every layer, before any import cleanup, for
|
||||
/// comparing against <see cref="Entities"/>. Display only: these share no instances
|
||||
/// with <see cref="Entities"/> and their IDs key nothing.
|
||||
/// </summary>
|
||||
public List<Entity> RawEntities { get; set; } = new List<Entity>();
|
||||
|
||||
/// <summary>
|
||||
/// Bends detected during import. Mutable — callers may add, remove,
|
||||
/// or replace entries before building the drawing.
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace OpenNest.IO
|
||||
return new CadImportResult
|
||||
{
|
||||
Entities = dxf.Entities,
|
||||
RawEntities = dxf.RawEntities,
|
||||
Bends = bends,
|
||||
BendRepairReports = repairReports,
|
||||
Bounds = dxf.Entities.GetBoundingBox(),
|
||||
|
||||
+6
-1
@@ -39,7 +39,12 @@ namespace OpenNest.IO
|
||||
);
|
||||
}
|
||||
|
||||
return new DxfImportResult { Entities = entities, Document = doc };
|
||||
return new DxfImportResult
|
||||
{
|
||||
Entities = entities,
|
||||
RawEntities = ConvertEntities(doc, _ => false, optimize: false),
|
||||
Document = doc,
|
||||
};
|
||||
}
|
||||
|
||||
public static List<Entity> GetGeometry(string path) =>
|
||||
|
||||
@@ -7,6 +7,12 @@ namespace OpenNest.IO
|
||||
public class DxfImportResult
|
||||
{
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The file's geometry on every layer, before any cleanup. These are separate
|
||||
/// instances from <see cref="Entities"/>.
|
||||
/// </summary>
|
||||
public List<Entity> RawEntities { get; set; } = new();
|
||||
public CadDocument Document { get; set; }
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -221,6 +221,11 @@ namespace OpenNest.IO
|
||||
{
|
||||
WalkChildren(root, transform, Layer.Default);
|
||||
|
||||
var raw = new List<Entity>();
|
||||
raw.AddRange(circles.CloneAll());
|
||||
raw.AddRange(lines.CloneAll());
|
||||
raw.AddRange(arcs.CloneAll());
|
||||
|
||||
GeometryOptimizer.Optimize(lines);
|
||||
GeometryOptimizer.Optimize(arcs);
|
||||
GeometryOptimizer.Deduplicate(circles);
|
||||
@@ -234,6 +239,7 @@ namespace OpenNest.IO
|
||||
return new DxfImportResult
|
||||
{
|
||||
Entities = entities,
|
||||
RawEntities = raw,
|
||||
Warnings = skipped
|
||||
.OrderBy(s => s.Key, StringComparer.Ordinal)
|
||||
.Select(s => $"Skipped {s.Value} <{s.Key}> element(s); convert them to paths to import them.")
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ACadSharp;
|
||||
using ACadSharp.IO;
|
||||
using CSMath;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Math;
|
||||
using CadArc = ACadSharp.Entities.Arc;
|
||||
using CadCircle = ACadSharp.Entities.Circle;
|
||||
using CadLayer = ACadSharp.Tables.Layer;
|
||||
using CadLine = ACadSharp.Entities.Line;
|
||||
|
||||
namespace OpenNest.Tests.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A 4 x 6 plate whose bottom edge is two collinear lines, with one hole drawn the way CAM
|
||||
/// exports do (two overlapping arcs and a seam chord) and one ETCH mark.
|
||||
/// </summary>
|
||||
public class CadImporterRawEntitiesTests : IDisposable
|
||||
{
|
||||
private readonly string directory = Directory.CreateTempSubdirectory("opennest-raw-").FullName;
|
||||
|
||||
public void Dispose() => Directory.Delete(directory, true);
|
||||
|
||||
[Fact]
|
||||
public void Dxf_RawEntitiesHoldTheFile_WhileEntitiesHoldTheCleanedGeometry()
|
||||
{
|
||||
var result = CadImporter.Import(WritePlate());
|
||||
|
||||
Assert.Empty(result.RawEntities.OfType<Circle>());
|
||||
Assert.Equal(2, result.RawEntities.OfType<Arc>().Count());
|
||||
Assert.Equal(7, result.RawEntities.OfType<Line>().Count());
|
||||
Assert.Contains(result.RawEntities, e => e.Layer.Name == "ETCH");
|
||||
|
||||
Assert.Single(result.Entities.OfType<Circle>());
|
||||
Assert.Empty(result.Entities.OfType<Arc>());
|
||||
Assert.Equal(4, result.Entities.OfType<Line>().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawEntities_ShareNoInstancesWithTheWorkingEntities()
|
||||
{
|
||||
var result = CadImporter.Import(WritePlate());
|
||||
|
||||
Assert.Empty(result.RawEntities.Intersect(result.Entities));
|
||||
Assert.Empty(result.RawEntities.Select(e => e.Layer).Intersect(result.Entities.Select(e => e.Layer)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawEntities_DoNotDisturbFileIds()
|
||||
{
|
||||
var path = WritePlate();
|
||||
|
||||
var first = CadImporter.Import(path);
|
||||
var second = CadImporter.Import(path);
|
||||
|
||||
Assert.Equal(first.Entities.Select(e => e.Id), second.Entities.Select(e => e.Id));
|
||||
Assert.Empty(first.RawEntities.Select(e => e.Id).Intersect(first.Entities.Select(e => e.Id)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Svg_RawEntitiesKeepWhatTheOptimizerMerges()
|
||||
{
|
||||
var path = Path.Combine(directory, "plate.svg");
|
||||
File.WriteAllText(
|
||||
path,
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='4in' height='4in' viewBox='0 0 4 4'>"
|
||||
+ "<line x1='0' y1='0' x2='2' y2='0'/><line x1='2' y1='0' x2='4' y2='0'/>"
|
||||
+ "<circle cx='2' cy='2' r='1'/><circle cx='2' cy='2' r='1'/></svg>"
|
||||
);
|
||||
|
||||
var result = CadImporter.Import(path);
|
||||
|
||||
Assert.Equal(2, result.RawEntities.OfType<Line>().Count());
|
||||
Assert.Equal(2, result.RawEntities.OfType<Circle>().Count());
|
||||
Assert.Single(result.Entities.OfType<Line>());
|
||||
Assert.Single(result.Entities.OfType<Circle>());
|
||||
Assert.Empty(result.RawEntities.Intersect(result.Entities));
|
||||
}
|
||||
|
||||
private string WritePlate()
|
||||
{
|
||||
var layer = new CadLayer("0");
|
||||
var etch = new CadLayer("ETCH");
|
||||
var document = new CadDocument();
|
||||
document.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Inches;
|
||||
|
||||
void Add(double x1, double y1, double x2, double y2, CadLayer on) =>
|
||||
document.Entities.Add(new CadLine(new XYZ(x1, y1, 0), new XYZ(x2, y2, 0)) { Layer = on });
|
||||
|
||||
Add(0, 0, 2, 0, layer);
|
||||
Add(2, 0, 4, 0, layer);
|
||||
Add(4, 0, 4, 6, layer);
|
||||
Add(4, 6, 0, 6, layer);
|
||||
Add(0, 6, 0, 0, layer);
|
||||
Add(1, 1, 1, 2, etch);
|
||||
|
||||
const double radius = 0.3125;
|
||||
document.Entities.Add(HoleArc(210, 30, radius, layer));
|
||||
document.Entities.Add(HoleArc(30, 215.5, radius, layer));
|
||||
var overshoot = OnHole(215.5, radius);
|
||||
var seam = OnHole(210, radius);
|
||||
Add(overshoot.X, overshoot.Y, seam.X, seam.Y, layer);
|
||||
|
||||
var path = Path.Combine(directory, "plate.dxf");
|
||||
DxfWriter.Write(path, document, false);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static CadArc HoleArc(double start, double end, double radius, CadLayer layer) =>
|
||||
new()
|
||||
{
|
||||
Center = new XYZ(2, 3, 0),
|
||||
Radius = radius,
|
||||
StartAngle = Angle.ToRadians(start),
|
||||
EndAngle = Angle.ToRadians(end),
|
||||
Layer = layer,
|
||||
};
|
||||
|
||||
private static Vector OnHole(double degrees, double radius)
|
||||
{
|
||||
var angle = Angle.ToRadians(degrees);
|
||||
return new Vector(2 + radius * System.Math.Cos(angle), 3 + radius * System.Math.Sin(angle));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
using ACadSharp;
|
||||
using ACadSharp.IO;
|
||||
using CSMath;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Math;
|
||||
using static OpenNest.WinForms.Tests.FormTesting;
|
||||
using CadArc = ACadSharp.Entities.Arc;
|
||||
using CadLayer = ACadSharp.Tables.Layer;
|
||||
using CadLine = ACadSharp.Entities.Line;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// The Original overlay shows the source file, while Simplify keeps working from the
|
||||
/// cleaned geometry.
|
||||
/// </summary>
|
||||
public class CadConverterOriginalOverlayTests
|
||||
{
|
||||
[Fact]
|
||||
public void Overlay_ShowsTheFileFromLoad_AndSimplifyStartsFromTheCleanedGeometry() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
var directory = Directory.CreateTempSubdirectory("opennest-overlay-").FullName;
|
||||
try
|
||||
{
|
||||
using var form = new CadConverterForm();
|
||||
Show(form);
|
||||
form.AddFile(WriteHole(directory));
|
||||
var view = Field<EntityView>(form, "entityView1");
|
||||
var item = Field<FileListControl>(form, "fileList").SelectedItem!;
|
||||
|
||||
Assert.Null(view.OriginalEntities);
|
||||
Field<CheckBox>(form, "chkShowOriginal").Checked = true;
|
||||
Assert.Same(item.RawEntities, view.OriginalEntities);
|
||||
Assert.Equal(2, view.OriginalEntities!.OfType<Arc>().Count());
|
||||
Assert.Empty(view.OriginalEntities.OfType<Circle>());
|
||||
Assert.Single(item.Entities.OfType<Circle>());
|
||||
Assert.Empty(item.Entities.OfType<Arc>());
|
||||
|
||||
// Simplify must not bring the joined hole's arcs back.
|
||||
Assert.Null(item.OriginalEntities);
|
||||
Invoke(form, "OnSimplifyClick", null, EventArgs.Empty);
|
||||
Assert.Single(item.OriginalEntities!.OfType<Circle>());
|
||||
Assert.Empty(item.OriginalEntities!.OfType<Arc>());
|
||||
Assert.Same(item.RawEntities, view.OriginalEntities);
|
||||
form.Close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void Overlay_FallsBackToThePreSimplifyEntities_WhenNoFileWasRead() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
using var form = new CadConverterForm();
|
||||
Show(form);
|
||||
form.LoadDrawings([Reopened()]);
|
||||
var view = Field<EntityView>(form, "entityView1");
|
||||
var item = Field<FileListControl>(form, "fileList").SelectedItem!;
|
||||
|
||||
Field<CheckBox>(form, "chkShowOriginal").Checked = true;
|
||||
Assert.Null(view.OriginalEntities);
|
||||
|
||||
Invoke(form, "OnSimplifyClick", null, EventArgs.Empty);
|
||||
Invoke(form, "OnShowOriginalChanged", null, EventArgs.Empty);
|
||||
|
||||
Assert.Null(item.RawEntities);
|
||||
Assert.Same(item.OriginalEntities, view.OriginalEntities);
|
||||
form.Close();
|
||||
});
|
||||
|
||||
private static string WriteHole(string directory)
|
||||
{
|
||||
var layer = new CadLayer("0");
|
||||
var document = new CadDocument();
|
||||
document.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Inches;
|
||||
|
||||
void Add(double x1, double y1, double x2, double y2) =>
|
||||
document.Entities.Add(new CadLine(new XYZ(x1, y1, 0), new XYZ(x2, y2, 0)) { Layer = layer });
|
||||
|
||||
Add(0, 0, 4, 0);
|
||||
Add(4, 0, 4, 6);
|
||||
Add(4, 6, 0, 6);
|
||||
Add(0, 6, 0, 0);
|
||||
|
||||
const double radius = 0.3125;
|
||||
document.Entities.Add(HoleArc(210, 30, radius, layer));
|
||||
document.Entities.Add(HoleArc(30, 215.5, radius, layer));
|
||||
var overshoot = OnHole(215.5, radius);
|
||||
var seam = OnHole(210, radius);
|
||||
Add(overshoot.X, overshoot.Y, seam.X, seam.Y);
|
||||
|
||||
var path = Path.Combine(directory, "hole.dxf");
|
||||
DxfWriter.Write(path, document, false);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static CadArc HoleArc(double start, double end, double radius, CadLayer layer) =>
|
||||
new()
|
||||
{
|
||||
Center = new XYZ(2, 3, 0),
|
||||
Radius = radius,
|
||||
StartAngle = Angle.ToRadians(start),
|
||||
EndAngle = Angle.ToRadians(end),
|
||||
Layer = layer,
|
||||
};
|
||||
|
||||
private static Vector OnHole(double degrees, double radius)
|
||||
{
|
||||
var angle = Angle.ToRadians(degrees);
|
||||
return new Vector(2 + radius * System.Math.Cos(angle), 3 + radius * System.Math.Sin(angle));
|
||||
}
|
||||
|
||||
private static Drawing Reopened()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, 10, 0), new Line(10, 0, 10, 6), new Line(10, 6, 0, 6), new Line(0, 6, 0, 0),
|
||||
};
|
||||
var layer = new Layer("0") { IsVisible = true };
|
||||
foreach (var e in entities)
|
||||
e.Layer = layer;
|
||||
return CadImporter.BuildDrawing(new CadImportResult { Entities = entities, Name = "reopened" }, entities,
|
||||
new List<Bend>(), 1, null, null);
|
||||
}
|
||||
|
||||
private static T Field<T>(object owner, string name) =>
|
||||
(T)typeof(CadConverterForm).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
|
||||
|
||||
private static void Invoke(CadConverterForm form, string name, params object?[] args) =>
|
||||
typeof(CadConverterForm).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(form, args);
|
||||
}
|
||||
@@ -107,15 +107,12 @@ namespace OpenNest.Controls
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
|
||||
// Draw original geometry overlay (faded, behind current)
|
||||
// Draw original geometry overlay (faded, behind current); filters don't apply to it
|
||||
if (OriginalEntities != null)
|
||||
{
|
||||
using var origPen = new Pen(Color.FromArgb(50, 255, 140, 40));
|
||||
foreach (var entity in OriginalEntities)
|
||||
{
|
||||
if (!IsEtchLayer(entity.Layer))
|
||||
DrawEntity(e.Graphics, entity, origPen);
|
||||
}
|
||||
DrawEntityShape(e.Graphics, entity, origPen);
|
||||
}
|
||||
|
||||
using (var excludedPen = new Pen(Color.FromArgb(90, 150, 150, 150)) { DashPattern = new float[] { 2, 4 } })
|
||||
@@ -302,9 +299,6 @@ namespace OpenNest.Controls
|
||||
private static readonly Color EtchRoleColor = Color.Magenta;
|
||||
private static readonly Color DisplayRoleColor = Color.FromArgb(120, 200, 220);
|
||||
|
||||
private static bool IsEtchLayer(Layer layer) =>
|
||||
string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private void DrawBendLines(Graphics g)
|
||||
{
|
||||
if (Bends == null || Bends.Count == 0)
|
||||
|
||||
@@ -17,7 +17,16 @@ namespace OpenNest.Controls
|
||||
public int Quantity { get; set; } = 1;
|
||||
public string Path { get; set; }
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
|
||||
/// <summary>The cleaned entities before Simplify replaced them; Simplify always restarts from these.</summary>
|
||||
public List<Entity> OriginalEntities { get; set; }
|
||||
|
||||
/// <summary>The source file's geometry before import cleanup, for the Original overlay; null when no file was read.</summary>
|
||||
public List<Entity> RawEntities { get; set; }
|
||||
|
||||
/// <summary>What the Original overlay shows: a drawing reopened from a nest has only its pre-Simplify entities.</summary>
|
||||
public List<Entity> OverlayEntities => RawEntities ?? OriginalEntities;
|
||||
|
||||
public List<Bend> Bends { get; set; } = new();
|
||||
public List<Breakoffs.Breakoff> Breakoffs { get; set; } = new();
|
||||
public List<TabbedCutouts.TabbedCutout> TabbedCutouts { get; set; } = new();
|
||||
|
||||
@@ -124,6 +124,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
Name = result.Name,
|
||||
Entities = result.Entities,
|
||||
RawEntities = result.RawEntities,
|
||||
Path = result.SourcePath,
|
||||
Quantity = 1,
|
||||
Customer = string.Empty,
|
||||
@@ -234,7 +235,7 @@ namespace OpenNest.Forms
|
||||
entityView1.SelectedBreakoffIndex = -1;
|
||||
entityView1.SelectedTabbedCutoutIndex = -1;
|
||||
entityView1.SelectedDimensionIndex = -1;
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item.OriginalEntities : null;
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item.OverlayEntities : null;
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||
@@ -521,6 +522,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(splitPath),
|
||||
Entities = result.Entities,
|
||||
RawEntities = result.RawEntities,
|
||||
Path = splitPath,
|
||||
Quantity = item.Quantity,
|
||||
Customer = item.Customer,
|
||||
@@ -1331,7 +1333,7 @@ namespace OpenNest.Forms
|
||||
private void OnShowOriginalChanged(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item?.OriginalEntities : null;
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item?.OverlayEntities : null;
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user