feat: separate DXF entities onto per-type layers, recognize SCRIBE marker
Build PepApi image / build-and-push (push) Successful in 45s

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 <noreply@anthropic.com>
This commit is contained in:
aj
2026-08-26 09:49:53 -04:00
co-authored by Claude Sonnet 5
parent 63f19493a0
commit 154574721e
4 changed files with 141 additions and 21 deletions
@@ -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<ACadSharp.Entities.Line>().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<ACadSharp.Entities.Arc>().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<ACadSharp.Entities.Line>().Single();
Assert.Equal("TEST.loop-000", line.Layer.Name);
}
}
@@ -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<LinearMove>(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<CircularMove>(Assert.Single(program));
Assert.Equal(EntityType.Scribe, move.Type);
}
}
+31 -21
View File
@@ -30,16 +30,17 @@ public static class DrawingDxfExporter
}
}
var layers = new Dictionary<string, Layer>();
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<string, Layer> 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<string, Layer> layers, Layer cutLayer, EntityType type) =>
type == EntityType.Cut ? cutLayer : GetOrCreateLayer(doc, layers, type.ToString());
private static Layer GetOrCreateLayer(CadDocument doc, Dictionary<string, Layer> 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);
+8
View File
@@ -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;
}