From 154574721e07031176b3b488825f22908a6657ba Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 26 Aug 2026 09:49:45 -0400 Subject: [PATCH] feat: separate DXF entities onto per-type layers, recognize SCRIBE marker DrawLoop previously drew every entity onto the single loop layer, so scribe/display marks were indistinguishable from cuts in the exported DXF. Non-cut entity types now get their own layer, and ProgramReader now parses the SCRIBE marker into EntityType.Scribe so scribed moves route to their own layer instead of falling through as untyped cuts. Co-Authored-By: Claude Sonnet 5 --- .../IO/DrawingDxfExporterTests.cs | 58 +++++++++++++++++++ PepLib.Core.Tests/IO/ProgramReaderTests.cs | 44 ++++++++++++++ PepLib.Core/IO/DrawingDxfExporter.cs | 52 ++++++++++------- PepLib.Core/IO/ProgramReader.cs | 8 +++ 4 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 PepLib.Core.Tests/IO/ProgramReaderTests.cs diff --git a/PepLib.Core.Tests/IO/DrawingDxfExporterTests.cs b/PepLib.Core.Tests/IO/DrawingDxfExporterTests.cs index bb6492c..fdb68f8 100644 --- a/PepLib.Core.Tests/IO/DrawingDxfExporterTests.cs +++ b/PepLib.Core.Tests/IO/DrawingDxfExporterTests.cs @@ -64,4 +64,62 @@ public class DrawingDxfExporterTests Assert.InRange(arc.StartAngle, 0, 360); Assert.InRange(arc.EndAngle, 0, 360); } + + [Fact] + public void Export_NonCutLinearMove_IsDrawnOnDedicatedLayerInsteadOfDropped() + { + var drawing = new Drawing(); + drawing.Info = new DrawingInfo { Name = "TEST" }; + var loop = new Loop { Name = "TEST.loop-000" }; + loop.Add(new RapidMove(new Vector(0, 0))); + loop.Add(new LinearMove(new Vector(10, 0)) { Type = EntityType.InternalLeadin }); + drawing.Loops.Add(loop); + + using var stream = DrawingDxfExporter.Export(drawing); + + stream.Position = 0; + using var reader = new ACadSharp.IO.DxfReader(stream); + var doc = reader.Read(); + var line = doc.Entities.OfType().Single(); + Assert.Equal("InternalLeadin", line.Layer.Name); + } + + [Fact] + public void Export_NonCutCircularMove_IsDrawnOnDedicatedLayerInsteadOfCutLayer() + { + var drawing = new Drawing(); + drawing.Info = new DrawingInfo { Name = "TEST" }; + var loop = new Loop { Name = "TEST.loop-000" }; + loop.Add(new RapidMove(new Vector(5, 0))); + loop.Add(new CircularMove(new Vector(-5, 5), new Vector(-5, 0), RotationType.CCW) { Type = EntityType.ExternalLeadout }); + drawing.Loops.Add(loop); + + using var stream = DrawingDxfExporter.Export(drawing); + + stream.Position = 0; + using var reader = new ACadSharp.IO.DxfReader(stream); + var doc = reader.Read(); + var arc = doc.Entities.OfType().Single(); + Assert.Equal("ExternalLeadout", arc.Layer.Name); + Assert.NotEqual("TEST.loop-000", arc.Layer.Name); + } + + [Fact] + public void Export_CutLinearMove_StaysOnLoopNamedLayer() + { + var drawing = new Drawing(); + drawing.Info = new DrawingInfo { Name = "TEST" }; + var loop = new Loop { Name = "TEST.loop-000" }; + loop.Add(new RapidMove(new Vector(0, 0))); + loop.Add(new LinearMove(new Vector(10, 0))); + drawing.Loops.Add(loop); + + using var stream = DrawingDxfExporter.Export(drawing); + + stream.Position = 0; + using var reader = new ACadSharp.IO.DxfReader(stream); + var doc = reader.Read(); + var line = doc.Entities.OfType().Single(); + Assert.Equal("TEST.loop-000", line.Layer.Name); + } } diff --git a/PepLib.Core.Tests/IO/ProgramReaderTests.cs b/PepLib.Core.Tests/IO/ProgramReaderTests.cs new file mode 100644 index 0000000..5c1806f --- /dev/null +++ b/PepLib.Core.Tests/IO/ProgramReaderTests.cs @@ -0,0 +1,44 @@ +using System.Text; +using PepLib.Codes; +using PepLib.Models; +using Xunit; + +namespace PepLib.Core.Tests.IO; + +public class ProgramReaderTests +{ + // ProgramReader reads fixed 200-byte ASCII records (no newlines), matching + // the raw record format found inside real .pep loop entries. + private const int RecordSize = 200; + + private static Stream RecordStream(params string[] lines) + { + var sb = new StringBuilder(); + foreach (var line in lines) + sb.Append(line.PadRight(RecordSize)); + + return new MemoryStream(Encoding.ASCII.GetBytes(sb.ToString())); + } + + [Fact] + public void Load_LinearMoveWithScribeMarker_SetsTypeToScribe() + { + using var stream = RecordStream("N50G1X1Y0:SCRIBE"); + + var program = Program.Load(stream); + + var move = Assert.IsType(Assert.Single(program)); + Assert.Equal(EntityType.Scribe, move.Type); + } + + [Fact] + public void Load_CircularMoveWithScribeMarker_SetsTypeToScribe() + { + using var stream = RecordStream("N50G2X1Y0I0.5J0:SCRIBE"); + + var program = Program.Load(stream); + + var move = Assert.IsType(Assert.Single(program)); + Assert.Equal(EntityType.Scribe, move.Type); + } +} diff --git a/PepLib.Core/IO/DrawingDxfExporter.cs b/PepLib.Core/IO/DrawingDxfExporter.cs index 461d1c7..dfc6ddf 100644 --- a/PepLib.Core/IO/DrawingDxfExporter.cs +++ b/PepLib.Core/IO/DrawingDxfExporter.cs @@ -30,16 +30,17 @@ public static class DrawingDxfExporter } } + var layers = new Dictionary(); + foreach (var loop in drawing.Loops) { var loopNumber = GetLoopNumber(loop.Name); if (subprogramIds.Contains(loopNumber)) continue; // drawn inline at SubProgramCall positions - var layer = new Layer(loop.Name); - doc.Layers.Add(layer); + var cutLayer = GetOrCreateLayer(doc, layers, loop.Name); - DrawLoop(doc, drawing, loop, layer, new Vector()); + DrawLoop(doc, drawing, loop, cutLayer, layers, new Vector()); } var buffer = new MemoryStream(); @@ -51,7 +52,7 @@ public static class DrawingDxfExporter return new MemoryStream(buffer.ToArray()); } - private static Vector DrawLoop(CadDocument doc, Drawing drawing, Loop loop, Layer layer, Vector startPos) + private static Vector DrawLoop(CadDocument doc, Drawing drawing, Loop loop, Layer cutLayer, Dictionary layers, Vector startPos) { var pos = startPos; @@ -65,21 +66,14 @@ public static class DrawingDxfExporter case CodeType.LinearMove: var lm = (LinearMove)code; - if (lm.Type == EntityType.Cut) + var lineEnd = Advance(pos, lm.EndPoint, loop.Mode); + doc.Entities.Add(new AcadLine { - var lineEnd = Advance(pos, lm.EndPoint, loop.Mode); - doc.Entities.Add(new AcadLine - { - StartPoint = ToXYZ(pos), - EndPoint = ToXYZ(lineEnd), - Layer = layer - }); - pos = lineEnd; - } - else - { - pos = Advance(pos, lm.EndPoint, loop.Mode); - } + StartPoint = ToXYZ(pos), + EndPoint = ToXYZ(lineEnd), + Layer = LayerFor(doc, layers, cutLayer, lm.Type) + }); + pos = lineEnd; break; case CodeType.CircularMove: @@ -97,13 +91,15 @@ public static class DrawingDxfExporter if (cm.Rotation == RotationType.CW) (startAngle, endAngle) = (endAngle, startAngle); + var arcLayer = LayerFor(doc, layers, cutLayer, cm.Type); + if (Math.Abs(startAngle - endAngle) < 1e-10) { doc.Entities.Add(new AcadCircle { Center = ToXYZ(arcCenter), Radius = radius, - Layer = layer + Layer = arcLayer }); } else @@ -114,7 +110,7 @@ public static class DrawingDxfExporter Radius = radius, StartAngle = startAngle, EndAngle = endAngle, - Layer = layer + Layer = arcLayer }); } @@ -125,7 +121,7 @@ public static class DrawingDxfExporter var call = (SubProgramCall)code; var subLoop = drawing.Loops.FirstOrDefault(l => l.Name == drawing.GetLoopName(call.LoopId)); if (subLoop != null) - DrawLoop(doc, drawing, subLoop, layer, pos); + DrawLoop(doc, drawing, subLoop, cutLayer, layers, pos); // pos unchanged — subprograms are closed shapes that return to start break; } @@ -134,6 +130,20 @@ public static class DrawingDxfExporter return pos; } + private static Layer LayerFor(CadDocument doc, Dictionary layers, Layer cutLayer, EntityType type) => + type == EntityType.Cut ? cutLayer : GetOrCreateLayer(doc, layers, type.ToString()); + + private static Layer GetOrCreateLayer(CadDocument doc, Dictionary layers, string name) + { + if (layers.TryGetValue(name, out var existing)) + return existing; + + var layer = new Layer(name); + doc.Layers.Add(layer); + layers[name] = layer; + return layer; + } + private static int GetLoopNumber(string loopName) { var idx = loopName.LastIndexOf(".loop-", StringComparison.OrdinalIgnoreCase); diff --git a/PepLib.Core/IO/ProgramReader.cs b/PepLib.Core/IO/ProgramReader.cs index e9cded1..72823ae 100644 --- a/PepLib.Core/IO/ProgramReader.cs +++ b/PepLib.Core/IO/ProgramReader.cs @@ -189,6 +189,10 @@ namespace PepLib.IO case "DISPLAY": type = EntityType.Display; break; + + case "SCRIBE": + type = EntityType.Scribe; + break; } break; } @@ -266,6 +270,10 @@ namespace PepLib.IO case "DISPLAY": type = EntityType.Display; break; + + case "SCRIBE": + type = EntityType.Scribe; + break; } break; }