diff --git a/OpenNest.Core/Geometry/GeometryOptimizer.cs b/OpenNest.Core/Geometry/GeometryOptimizer.cs index ff9a888..0ec62fb 100644 --- a/OpenNest.Core/Geometry/GeometryOptimizer.cs +++ b/OpenNest.Core/Geometry/GeometryOptimizer.cs @@ -76,6 +76,9 @@ namespace OpenNest.Geometry if (line1 == line2) return false; + if (line1.Layer?.Name != line2.Layer?.Name) + return false; + if (!line1.IsCollinearTo(line2)) return false; @@ -113,9 +116,9 @@ namespace OpenNest.Geometry var b = b1 < b2 ? b1 : b2; if (!line1.IsVertical() && line1.Slope() < 0) - lineOut = new Line(new Vector(l, t), new Vector(r, b)); + lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color }; else - lineOut = new Line(new Vector(l, b), new Vector(r, t)); + lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color }; return true; } @@ -127,6 +130,9 @@ namespace OpenNest.Geometry if (arc1 == arc2) return false; + if (arc1.Layer?.Name != arc2.Layer?.Name) + return false; + if (arc1.Center != arc2.Center) return false; @@ -161,7 +167,7 @@ namespace OpenNest.Geometry if (startAngle < 0) startAngle += Angle.TwoPI; if (endAngle < 0) endAngle += Angle.TwoPI; - arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle); + arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color }; return true; } diff --git a/OpenNest.IO/DxfImporter.cs b/OpenNest.IO/DxfImporter.cs index 8bc8741..2f2482c 100644 --- a/OpenNest.IO/DxfImporter.cs +++ b/OpenNest.IO/DxfImporter.cs @@ -24,9 +24,10 @@ namespace OpenNest.IO foreach (var entity in doc.Entities) { - // Skip bend line entities — they are converted to Bend objects - // separately via bend detection, not cut geometry. - if (IsBendLayer(entity.Layer?.Name)) + // Skip bend/etch entities — bends are converted to Bend objects + // separately via bend detection, and etch marks are generated from + // bends during DXF export. Neither should be treated as cut geometry. + if (IsNonCutLayer(entity.Layer?.Name)) continue; switch (entity) @@ -137,9 +138,10 @@ namespace OpenNest.IO return success; } - private static bool IsBendLayer(string layerName) + private static bool IsNonCutLayer(string layerName) { - return string.Equals(layerName, "BEND", System.StringComparison.OrdinalIgnoreCase); + return string.Equals(layerName, "BEND", System.StringComparison.OrdinalIgnoreCase) + || string.Equals(layerName, "ETCH", System.StringComparison.OrdinalIgnoreCase); } } } diff --git a/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs b/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs new file mode 100644 index 0000000..04bd91a --- /dev/null +++ b/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs @@ -0,0 +1,219 @@ +using ACadSharp.IO; +using OpenNest.Bending; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.Shapes; + +namespace OpenNest.Tests.Splitting; + +public class SplitDxfWriterEtchLayerTests +{ + [Fact] + public void Write_DrawingWithUpBend_EtchLinesHaveEtchLayer() + { + // Create a simple rectangular drawing with an up bend + var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + drawing.Bends = new List + { + new Bend + { + StartPoint = new Vector(0, 25), + EndPoint = new Vector(100, 25), + Direction = BendDirection.Up, + Angle = 90, + Radius = 0.06, + NoteText = "UP 90° R0.06" + } + }; + + var tempPath = Path.Combine(Path.GetTempPath(), $"etch_layer_test_{Guid.NewGuid()}.dxf"); + try + { + var writer = new SplitDxfWriter(); + writer.Write(tempPath, drawing); + + // Re-read the DXF and check entity layers + using var reader = new DxfReader(tempPath); + var doc = reader.Read(); + + var etchEntities = new List(); + var allEntities = new List<(string LayerName, string Type)>(); + + foreach (var entity in doc.Entities) + { + var layerName = entity.Layer?.Name ?? "(null)"; + allEntities.Add((layerName, entity.GetType().Name)); + + // Etch lines are short lines along the bend direction at the ends + if (entity is ACadSharp.Entities.Line line) + { + // Check if this line is an etch mark (short, near the bend Y=25) + var midY = (line.StartPoint.Y + line.EndPoint.Y) / 2; + var length = System.Math.Sqrt( + System.Math.Pow(line.EndPoint.X - line.StartPoint.X, 2) + + System.Math.Pow(line.EndPoint.Y - line.StartPoint.Y, 2)); + + if (System.Math.Abs(midY - 25) < 0.1 && length <= 1.5 && layerName != "BEND") + { + etchEntities.Add(entity); + } + } + } + + // Should have etch lines (up bend with length 100 > 3*EtchLength, so 2 etch dashes) + Assert.True(etchEntities.Count >= 2, + $"Expected at least 2 etch lines, found {etchEntities.Count}. " + + $"All entities: {string.Join(", ", allEntities.Select(e => $"{e.Type}@{e.LayerName}"))}"); + + // ALL etch lines should be on the ETCH layer, not layer 0 + foreach (var etch in etchEntities) + { + var layerName = etch.Layer?.Name ?? "(null)"; + Assert.Equal("ETCH", layerName); + } + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + + [Fact] + public void Write_SplitDrawingWithUpBend_EtchLinesHaveEtchLayer() + { + // Create a drawing, split it, then verify etch layers in the split DXFs + var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + drawing.Bends = new List + { + new Bend + { + StartPoint = new Vector(0, 25), + EndPoint = new Vector(100, 25), + Direction = BendDirection.Up, + Angle = 90, + Radius = 0.06, + NoteText = "UP 90° R0.06" + } + }; + + var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; + var parameters = new SplitParameters { Type = SplitType.Straight }; + var results = DrawingSplitter.Split(drawing, splitLines, parameters); + + Assert.Equal(2, results.Count); + + foreach (var splitDrawing in results) + { + // Each split piece should have the bend (clipped to region) + Assert.NotNull(splitDrawing.Bends); + Assert.True(splitDrawing.Bends.Count > 0, $"{splitDrawing.Name} should have bends"); + + var tempPath = Path.Combine(Path.GetTempPath(), $"split_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"); + try + { + var writer = new SplitDxfWriter(); + writer.Write(tempPath, splitDrawing); + + // Re-read and verify + using var reader = new DxfReader(tempPath); + var doc = reader.Read(); + + var entitySummary = new List(); + var etchLayerEntities = new List(); + var layer0Entities = new List(); + + foreach (var entity in doc.Entities) + { + var layerName = entity.Layer?.Name ?? "(null)"; + entitySummary.Add($"{entity.GetType().Name}@{layerName}"); + + if (string.Equals(layerName, "ETCH", StringComparison.OrdinalIgnoreCase)) + etchLayerEntities.Add(entity); + else if (string.Equals(layerName, "0", StringComparison.OrdinalIgnoreCase)) + layer0Entities.Add(entity); + } + + // Should have etch entities + Assert.True(etchLayerEntities.Count > 0, + $"{splitDrawing.Name}: No entities on ETCH layer. " + + $"All: {string.Join(", ", entitySummary)}"); + + // No entities should be on layer 0 + Assert.True(layer0Entities.Count == 0, + $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 " + + $"(expected all on CUT/BEND/ETCH). " + + $"All: {string.Join(", ", entitySummary)}"); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + } + + [Fact] + public void Write_ReImport_EtchEntitiesFilteredFromCutGeometry() + { + // After re-import, ETCH entities should be filtered (like BEND) since + // etch marks are generated from bends, not treated as cut geometry. + var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + drawing.Bends = new List + { + new Bend + { + StartPoint = new Vector(0, 25), + EndPoint = new Vector(100, 25), + Direction = BendDirection.Up, + Angle = 90, + Radius = 0.06, + NoteText = "UP 90° R0.06" + } + }; + + var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; + var parameters = new SplitParameters { Type = SplitType.Straight }; + var results = DrawingSplitter.Split(drawing, splitLines, parameters); + + foreach (var splitDrawing in results) + { + var tempPath = Path.Combine(Path.GetTempPath(), $"reimport_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"); + try + { + var writer = new SplitDxfWriter(); + writer.Write(tempPath, splitDrawing); + + // Re-import via DxfImporter (same path as CadConverterForm) + var importer = new DxfImporter(); + var result = importer.Import(tempPath); + + // ETCH entities should be filtered during import (like BEND) + var etchEntities = result.Entities + .Where(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var layer0Entities = result.Entities + .Where(e => string.Equals(e.Layer?.Name, "0", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Assert.True(etchEntities.Count == 0, + $"{splitDrawing.Name}: ETCH entities should be filtered during import, found {etchEntities.Count}"); + + Assert.True(layer0Entities.Count == 0, + $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 after re-import"); + + // All imported entities should be on CUT layer (cut geometry only) + Assert.True(result.Entities.Count > 0, $"{splitDrawing.Name}: Should have cut geometry"); + Assert.True(result.Entities.All(e => string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase)), + $"{splitDrawing.Name}: All imported entities should be on CUT layer. " + + $"Found: {string.Join(", ", result.Entities.Select(e => e.Layer?.Name ?? "(null)").Distinct())}"); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + } +} diff --git a/OpenNest/Controls/EntityView.cs b/OpenNest/Controls/EntityView.cs index c717c3d..d90f517 100644 --- a/OpenNest/Controls/EntityView.cs +++ b/OpenNest/Controls/EntityView.cs @@ -84,6 +84,8 @@ namespace OpenNest.Controls DrawEntity(e.Graphics, entity, pen); } + DrawEtchMarks(e.Graphics); + #if DRAW_OFFSET var offsetShape = new Shape(); @@ -185,6 +187,46 @@ namespace OpenNest.Controls private static bool IsEtchLayer(Layer layer) => string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase); + private void DrawEtchMarks(Graphics g) + { + if (Bends == null || Bends.Count == 0) + return; + + using var etchPen = new Pen(Color.Green, 1.5f); + var etchLength = 1.0; + + foreach (var bend in Bends) + { + if (bend.Direction != BendDirection.Up) + continue; + + var start = bend.StartPoint; + var end = bend.EndPoint; + var length = bend.Length; + + if (length < etchLength * 3.0) + { + var pt1 = PointWorldToGraph(start); + var pt2 = PointWorldToGraph(end); + g.DrawLine(etchPen, pt1, pt2); + } + else + { + var angle = start.AngleTo(end); + var dx = System.Math.Cos(angle) * etchLength; + var dy = System.Math.Sin(angle) * etchLength; + + var s1 = PointWorldToGraph(start); + var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy)); + g.DrawLine(etchPen, s1, e1); + + var s2 = PointWorldToGraph(end); + var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy)); + g.DrawLine(etchPen, s2, e2); + } + } + } + private void DrawBendLines(Graphics g) { if (Bends == null || Bends.Count == 0)