fix: prevent etch line layers from defaulting to layer 0 after split

DxfImporter now filters ETCH entities (like BEND) since etch marks are
generated from bends during export, not cut geometry. GeometryOptimizer
no longer merges lines/arcs across different layers and preserves layer
and color on merged entities. EntityView draws etch marks directly from
the Bends list so they remain visible without relying on imported ETCH
entities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 22:31:28 -04:00
parent cbabf5e9d1
commit 12173204d1
4 changed files with 277 additions and 8 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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<Bend>
{
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<ACadSharp.Entities.Entity>();
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<Bend>
{
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<SplitLine> { 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<string>();
var etchLayerEntities = new List<ACadSharp.Entities.Entity>();
var layer0Entities = new List<ACadSharp.Entities.Entity>();
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<Bend>
{
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<SplitLine> { 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);
}
}
}
}

View File

@@ -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)