feat: serialize and deserialize hole sub-programs in nest file format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,10 +71,68 @@ namespace OpenNest.IO
|
||||
|
||||
var reader = new ProgramReader(memStream);
|
||||
programs[i] = reader.Read();
|
||||
|
||||
// Read sub-programs if present
|
||||
var subsEntry = zipArchive.GetEntry($"programs/program-{i}-subs");
|
||||
if (subsEntry != null)
|
||||
{
|
||||
using var subsStream = subsEntry.Open();
|
||||
ReadSubPrograms(programs[i], subsStream);
|
||||
}
|
||||
}
|
||||
return programs;
|
||||
}
|
||||
|
||||
private static void ReadSubPrograms(Program parent, Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream);
|
||||
var currentId = -1;
|
||||
var lines = new List<string>();
|
||||
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
|
||||
if (trimmed.StartsWith(":") && int.TryParse(trimmed.Substring(1), out var id))
|
||||
{
|
||||
// Flush previous sub-program
|
||||
if (currentId >= 0 && lines.Count > 0)
|
||||
parent.SubPrograms[currentId] = ParseSubProgram(lines);
|
||||
|
||||
currentId = id;
|
||||
lines.Clear();
|
||||
}
|
||||
else if (trimmed == "M99")
|
||||
{
|
||||
if (currentId >= 0 && lines.Count > 0)
|
||||
parent.SubPrograms[currentId] = ParseSubProgram(lines);
|
||||
|
||||
currentId = -1;
|
||||
lines.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up SubProgramCall.Program references
|
||||
foreach (var code in parent.Codes)
|
||||
{
|
||||
if (code is SubProgramCall call && parent.SubPrograms.TryGetValue(call.Id, out var sub))
|
||||
call.Program = sub;
|
||||
}
|
||||
}
|
||||
|
||||
private static Program ParseSubProgram(List<string> lines)
|
||||
{
|
||||
var text = string.Join("\n", lines);
|
||||
var memStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
|
||||
var reader = new ProgramReader(memStream);
|
||||
return reader.Read();
|
||||
}
|
||||
|
||||
private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> ReadEntitySets(int count)
|
||||
{
|
||||
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>)>();
|
||||
|
||||
@@ -308,8 +308,32 @@ namespace OpenNest.IO
|
||||
WriteDrawing(stream, kvp.Value);
|
||||
|
||||
var entry = zipArchive.CreateEntry(name);
|
||||
using var entryStream = entry.Open();
|
||||
stream.CopyTo(entryStream);
|
||||
using (var entryStream = entry.Open())
|
||||
{
|
||||
stream.CopyTo(entryStream);
|
||||
}
|
||||
|
||||
// Write sub-programs if present
|
||||
if (kvp.Value.Program.SubPrograms.Count > 0)
|
||||
WriteSubPrograms(zipArchive, kvp.Key, kvp.Value.Program.SubPrograms);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSubPrograms(ZipArchive zipArchive, int drawingId, Dictionary<int, Program> subPrograms)
|
||||
{
|
||||
var entry = zipArchive.CreateEntry($"programs/program-{drawingId}-subs");
|
||||
using var entryStream = entry.Open();
|
||||
using var writer = new StreamWriter(entryStream, Encoding.UTF8);
|
||||
|
||||
foreach (var kvp in subPrograms.OrderBy(k => k.Key))
|
||||
{
|
||||
writer.WriteLine($":{kvp.Key}");
|
||||
writer.WriteLine(kvp.Value.Mode == Mode.Absolute ? "G90" : "G91");
|
||||
|
||||
foreach (var code in kvp.Value.Codes)
|
||||
writer.WriteLine(GetCodeString(code));
|
||||
|
||||
writer.WriteLine("M99");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +472,9 @@ namespace OpenNest.IO
|
||||
case CodeType.SubProgramCall:
|
||||
{
|
||||
var subProgramCall = (SubProgramCall)code;
|
||||
break;
|
||||
var x = System.Math.Round(subProgramCall.Offset.X, OutputPrecision).ToString(CoordinateFormat);
|
||||
var y = System.Math.Round(subProgramCall.Offset.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||
return $"G65P{subProgramCall.Id}X{x}Y{y}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -374,6 +374,8 @@ namespace OpenNest.IO
|
||||
{
|
||||
var p = 0;
|
||||
var r = 0.0;
|
||||
var x = 0.0;
|
||||
var y = 0.0;
|
||||
|
||||
while (section == CodeSection.SubProgram)
|
||||
{
|
||||
@@ -395,13 +397,26 @@ namespace OpenNest.IO
|
||||
r = double.Parse(code.Value);
|
||||
break;
|
||||
|
||||
case 'X':
|
||||
x = double.Parse(code.Value);
|
||||
break;
|
||||
|
||||
case 'Y':
|
||||
y = double.Parse(code.Value);
|
||||
break;
|
||||
|
||||
default:
|
||||
section = CodeSection.Unknown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
program.Codes.Add(new SubProgramCall() { Id = p, Rotation = r });
|
||||
program.Codes.Add(new SubProgramCall
|
||||
{
|
||||
Id = p,
|
||||
Rotation = r,
|
||||
Offset = new Geometry.Vector(x, y)
|
||||
});
|
||||
}
|
||||
|
||||
private Code GetNextCode()
|
||||
|
||||
75
OpenNest.Tests/IO/SubProgramSerializationTests.cs
Normal file
75
OpenNest.Tests/IO/SubProgramSerializationTests.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.IO;
|
||||
|
||||
public class SubProgramSerializationTests
|
||||
{
|
||||
[Fact]
|
||||
public void NestWriter_WritesSubProgramCall_WithOffset()
|
||||
{
|
||||
var nest = CreateNestWithHoleSubProgram();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
var reader = new NestReader(stream);
|
||||
var loaded = reader.Read();
|
||||
|
||||
var drawing = loaded.Drawings.First();
|
||||
var calls = drawing.Program.Codes.OfType<SubProgramCall>().ToList();
|
||||
Assert.Single(calls);
|
||||
Assert.Equal(5, calls[0].Offset.X, 1);
|
||||
Assert.Equal(5, calls[0].Offset.Y, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestWriter_WritesSubPrograms_AndRestoresOnLoad()
|
||||
{
|
||||
var nest = CreateNestWithHoleSubProgram();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
var reader = new NestReader(stream);
|
||||
var loaded = reader.Read();
|
||||
|
||||
var drawing = loaded.Drawings.First();
|
||||
Assert.True(drawing.Program.SubPrograms.Count > 0);
|
||||
|
||||
var call = drawing.Program.Codes.OfType<SubProgramCall>().First();
|
||||
Assert.True(drawing.Program.SubPrograms.ContainsKey(call.Id));
|
||||
}
|
||||
|
||||
private static Nest CreateNestWithHoleSubProgram()
|
||||
{
|
||||
var sub = new Program(Mode.Incremental);
|
||||
sub.Codes.Add(new LinearMove(0.1, 0) { Layer = LayerType.Leadin });
|
||||
sub.Codes.Add(new ArcMove(new Vector(0, 0), new Vector(-0.5, 0), RotationType.CW));
|
||||
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.SubPrograms[42] = sub;
|
||||
pgm.Codes.Add(new SubProgramCall { Id = 42, Program = sub, Offset = new Vector(5, 5) });
|
||||
// Add perimeter so the drawing has non-zero geometry
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
|
||||
var drawing = new Drawing("TestPart") { Program = pgm };
|
||||
var nest = new Nest();
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = new Plate { Size = new Size(48, 96) };
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
return nest;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user