feat(io): persist part cutting programs and plate parameters

This commit is contained in:
aj
2026-09-28 19:17:59 -04:00
parent 74302761ce
commit b254a0bd3e
16 changed files with 1765 additions and 265 deletions
+97
View File
@@ -0,0 +1,97 @@
using OpenNest.CNC.CuttingStrategy;
namespace OpenNest.IO;
/// <summary>
/// JSON-safe cutting parameters shared by nest files and desktop settings.
/// Lead and tab discriminators avoid serializing abstract domain types.
/// </summary>
public class CuttingParametersDto
{
public int Id { get; set; }
public string MachineName { get; set; }
public string MaterialName { get; set; }
public string Grade { get; set; }
public double Thickness { get; set; }
public double Kerf { get; set; }
public double PartSpacing { get; set; }
public LeadInDto ExternalLeadIn { get; set; }
public LeadOutDto ExternalLeadOut { get; set; }
public LeadInDto InternalLeadIn { get; set; }
public LeadOutDto InternalLeadOut { get; set; }
public LeadInDto ArcCircleLeadIn { get; set; }
public LeadOutDto ArcCircleLeadOut { get; set; }
public bool TabsEnabled { get; set; }
// Legacy settings only stored tabWidth. A missing TabConfig uses that width;
// Type = "None" explicitly records a null tab in a nest parameter snapshot.
public double TabWidth { get; set; }
public TabDto TabConfig { get; set; }
public double PierceClearance { get; set; }
public bool RoundLeadInAngles { get; set; }
public double LeadInAngleIncrement { get; set; }
public double AutoTabMinSize { get; set; }
public double AutoTabMaxSize { get; set; }
public SequenceDto Sequencing { get; set; } = new();
public AssignmentDto Assignment { get; set; } = new();
public class LeadInDto
{
public string Type { get; set; } = "None";
public double Length { get; set; }
public double ApproachAngle { get; set; }
public double Radius { get; set; }
public double LineLength { get; set; }
public double ArcRadius { get; set; }
public double Kerf { get; set; }
public double Length1 { get; set; }
public double Angle1 { get; set; }
public double Length2 { get; set; }
public double Angle2 { get; set; }
}
public class LeadOutDto
{
public string Type { get; set; } = "None";
public double Length { get; set; }
public double ApproachAngle { get; set; }
public double Radius { get; set; }
public double GapSize { get; set; }
}
public class TabDto
{
public string Type { get; set; } = "None";
public double Size { get; set; }
public LeadInDto TabLeadIn { get; set; }
public LeadOutDto TabLeadOut { get; set; }
public double CutoutMinWidth { get; set; }
public double CutoutMinHeight { get; set; }
public double CutoutMaxWidth { get; set; }
public double CutoutMaxHeight { get; set; }
public int MachineTabId { get; set; }
public double BreakerDepth { get; set; }
public double BreakerLeadInLength { get; set; }
public double BreakerAngle { get; set; }
}
public class SequenceDto
{
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
public double SmallCutoutWidth { get; set; } = 1.5;
public double SmallCutoutHeight { get; set; } = 1.5;
public double MediumCutoutWidth { get; set; } = 8.0;
public double MediumCutoutHeight { get; set; } = 8.0;
public double DistanceMediumSmall { get; set; }
public bool AlternateRowsColumns { get; set; } = true;
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
}
public class AssignmentDto
{
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
public string Preference { get; set; } = "ILAT";
public double MinGeometryLength { get; set; } = 0.01;
}
}
+291
View File
@@ -0,0 +1,291 @@
using System;
using System.Text.Json;
using OpenNest.CNC.CuttingStrategy;
using static OpenNest.IO.CuttingParametersDto;
namespace OpenNest.IO;
public static class CuttingParametersSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
/// <summary>Writes desktop settings, retaining the legacy null-tab width fallback.</summary>
public static string Serialize(CuttingParameters parameters)
{
ArgumentNullException.ThrowIfNull(parameters);
var dto = ToDto(parameters);
if (parameters.TabConfig == null)
dto.TabConfig = null;
return JsonSerializer.Serialize(dto, JsonOptions);
}
/// <summary>Reads desktop settings with their historical defaults and angle fallback.</summary>
public static CuttingParameters Deserialize(string json)
{
var dto = JsonSerializer.Deserialize<CuttingParametersDto>(json, JsonOptions);
var parameters = FromDto(dto) ?? new CuttingParameters();
if (parameters.LeadInAngleIncrement <= 0)
parameters.LeadInAngleIncrement = 5.0;
return parameters;
}
/// <summary>Captures an owned snapshot. Null stays null for absent plate parameters.</summary>
public static CuttingParametersDto ToDto(CuttingParameters parameters)
{
if (parameters == null)
return null;
return new CuttingParametersDto
{
Id = parameters.Id,
MachineName = parameters.MachineName,
MaterialName = parameters.MaterialName,
Grade = parameters.Grade,
Thickness = parameters.Thickness,
Kerf = parameters.Kerf,
PartSpacing = parameters.PartSpacing,
ExternalLeadIn = ToLeadInDto(parameters.ExternalLeadIn),
ExternalLeadOut = ToLeadOutDto(parameters.ExternalLeadOut),
InternalLeadIn = ToLeadInDto(parameters.InternalLeadIn),
InternalLeadOut = ToLeadOutDto(parameters.InternalLeadOut),
ArcCircleLeadIn = ToLeadInDto(parameters.ArcCircleLeadIn),
ArcCircleLeadOut = ToLeadOutDto(parameters.ArcCircleLeadOut),
TabsEnabled = parameters.TabsEnabled,
TabWidth = parameters.TabConfig?.Size ?? 0.25,
TabConfig = ToTabDto(parameters.TabConfig),
PierceClearance = parameters.PierceClearance,
RoundLeadInAngles = parameters.RoundLeadInAngles,
LeadInAngleIncrement = parameters.LeadInAngleIncrement,
AutoTabMinSize = parameters.AutoTabMinSize,
AutoTabMaxSize = parameters.AutoTabMaxSize,
Sequencing = ToSequenceDto(parameters.Sequencing),
Assignment = ToAssignmentDto(parameters.Assignment),
};
}
/// <summary>Restores an owned snapshot without the desktop settings' value normalization.</summary>
public static CuttingParameters FromDto(CuttingParametersDto dto)
{
if (dto == null)
return null;
return new CuttingParameters
{
Id = dto.Id,
MachineName = dto.MachineName,
MaterialName = dto.MaterialName,
Grade = dto.Grade,
Thickness = dto.Thickness,
Kerf = dto.Kerf,
PartSpacing = dto.PartSpacing,
ExternalLeadIn = FromLeadInDto(dto.ExternalLeadIn),
ExternalLeadOut = FromLeadOutDto(dto.ExternalLeadOut),
InternalLeadIn = FromLeadInDto(dto.InternalLeadIn),
InternalLeadOut = FromLeadOutDto(dto.InternalLeadOut),
ArcCircleLeadIn = FromLeadInDto(dto.ArcCircleLeadIn),
ArcCircleLeadOut = FromLeadOutDto(dto.ArcCircleLeadOut),
TabsEnabled = dto.TabsEnabled,
TabConfig = dto.TabConfig == null
? new NormalTab { Size = dto.TabWidth }
: FromTabDto(dto.TabConfig),
PierceClearance = dto.PierceClearance,
RoundLeadInAngles = dto.RoundLeadInAngles,
LeadInAngleIncrement = dto.LeadInAngleIncrement,
AutoTabMinSize = dto.AutoTabMinSize,
AutoTabMaxSize = dto.AutoTabMaxSize,
Sequencing = FromSequenceDto(dto.Sequencing),
Assignment = FromAssignmentDto(dto.Assignment),
};
}
private static LeadInDto ToLeadInDto(LeadIn leadIn) => leadIn switch
{
LineLeadIn line => new LeadInDto
{
Type = "Line",
Length = line.Length,
ApproachAngle = line.ApproachAngle,
},
ArcLeadIn arc => new LeadInDto { Type = "Arc", Radius = arc.Radius },
LineArcLeadIn lineArc => new LeadInDto
{
Type = "LineArc",
LineLength = lineArc.LineLength,
ArcRadius = lineArc.ArcRadius,
ApproachAngle = lineArc.ApproachAngle,
},
CleanHoleLeadIn cleanHole => new LeadInDto
{
Type = "CleanHole",
LineLength = cleanHole.LineLength,
ArcRadius = cleanHole.ArcRadius,
Kerf = cleanHole.Kerf,
},
LineLineLeadIn lineLine => new LeadInDto
{
Type = "LineLine",
Length1 = lineLine.Length1,
Angle1 = lineLine.ApproachAngle1,
Length2 = lineLine.Length2,
Angle2 = lineLine.ApproachAngle2,
},
_ => new LeadInDto { Type = "None" },
};
private static LeadIn FromLeadInDto(LeadInDto dto) => dto?.Type switch
{
"Line" => new LineLeadIn { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
"Arc" => new ArcLeadIn { Radius = dto.Radius },
"LineArc" => new LineArcLeadIn
{
LineLength = dto.LineLength,
ArcRadius = dto.ArcRadius,
ApproachAngle = dto.ApproachAngle,
},
"CleanHole" => new CleanHoleLeadIn
{
LineLength = dto.LineLength,
ArcRadius = dto.ArcRadius,
Kerf = dto.Kerf,
},
"LineLine" => new LineLineLeadIn
{
Length1 = dto.Length1,
ApproachAngle1 = dto.Angle1,
Length2 = dto.Length2,
ApproachAngle2 = dto.Angle2,
},
_ => new NoLeadIn(),
};
private static LeadOutDto ToLeadOutDto(LeadOut leadOut) => leadOut switch
{
LineLeadOut line => new LeadOutDto
{
Type = "Line",
Length = line.Length,
ApproachAngle = line.ApproachAngle,
},
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
_ => new LeadOutDto { Type = "None" },
};
private static LeadOut FromLeadOutDto(LeadOutDto dto) => dto?.Type switch
{
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
"Arc" => new ArcLeadOut { Radius = dto.Radius },
_ => new NoLeadOut(),
};
private static TabDto ToTabDto(Tab tab)
{
var dto = tab switch
{
NormalTab normal => new TabDto
{
Type = "Normal",
CutoutMinWidth = normal.CutoutMinWidth,
CutoutMinHeight = normal.CutoutMinHeight,
CutoutMaxWidth = normal.CutoutMaxWidth,
CutoutMaxHeight = normal.CutoutMaxHeight,
},
MachineTab machine => new TabDto { Type = "Machine", MachineTabId = machine.MachineTabId },
BreakerTab breaker => new TabDto
{
Type = "Breaker",
BreakerDepth = breaker.BreakerDepth,
BreakerLeadInLength = breaker.BreakerLeadInLength,
BreakerAngle = breaker.BreakerAngle,
},
_ => new TabDto { Type = "None" },
};
if (tab != null)
{
dto.Size = tab.Size;
dto.TabLeadIn = tab.TabLeadIn == null ? null : ToLeadInDto(tab.TabLeadIn);
dto.TabLeadOut = tab.TabLeadOut == null ? null : ToLeadOutDto(tab.TabLeadOut);
}
return dto;
}
private static Tab FromTabDto(TabDto dto)
{
var tab = dto.Type switch
{
"Normal" => (Tab)new NormalTab
{
CutoutMinWidth = dto.CutoutMinWidth,
CutoutMinHeight = dto.CutoutMinHeight,
CutoutMaxWidth = dto.CutoutMaxWidth,
CutoutMaxHeight = dto.CutoutMaxHeight,
},
"Machine" => new MachineTab { MachineTabId = dto.MachineTabId },
"Breaker" => new BreakerTab
{
BreakerDepth = dto.BreakerDepth,
BreakerLeadInLength = dto.BreakerLeadInLength,
BreakerAngle = dto.BreakerAngle,
},
_ => null,
};
if (tab != null)
{
tab.Size = dto.Size;
tab.TabLeadIn = dto.TabLeadIn == null ? null : FromLeadInDto(dto.TabLeadIn);
tab.TabLeadOut = dto.TabLeadOut == null ? null : FromLeadOutDto(dto.TabLeadOut);
}
return tab;
}
private static SequenceDto ToSequenceDto(SequenceParameters parameters) => parameters == null
? null
: new SequenceDto
{
Method = parameters.Method,
SmallCutoutWidth = parameters.SmallCutoutWidth,
SmallCutoutHeight = parameters.SmallCutoutHeight,
MediumCutoutWidth = parameters.MediumCutoutWidth,
MediumCutoutHeight = parameters.MediumCutoutHeight,
DistanceMediumSmall = parameters.DistanceMediumSmall,
AlternateRowsColumns = parameters.AlternateRowsColumns,
AlternateCutoutsWithinRowColumn = parameters.AlternateCutoutsWithinRowColumn,
MinDistanceBetweenRowsColumns = parameters.MinDistanceBetweenRowsColumns,
};
private static SequenceParameters FromSequenceDto(SequenceDto dto) => dto == null
? null
: new SequenceParameters
{
Method = dto.Method,
SmallCutoutWidth = dto.SmallCutoutWidth,
SmallCutoutHeight = dto.SmallCutoutHeight,
MediumCutoutWidth = dto.MediumCutoutWidth,
MediumCutoutHeight = dto.MediumCutoutHeight,
DistanceMediumSmall = dto.DistanceMediumSmall,
AlternateRowsColumns = dto.AlternateRowsColumns,
AlternateCutoutsWithinRowColumn = dto.AlternateCutoutsWithinRowColumn,
MinDistanceBetweenRowsColumns = dto.MinDistanceBetweenRowsColumns,
};
private static AssignmentDto ToAssignmentDto(AssignmentParameters parameters) => parameters == null
? null
: new AssignmentDto
{
Method = parameters.Method,
Preference = parameters.Preference,
MinGeometryLength = parameters.MinGeometryLength,
};
private static AssignmentParameters FromAssignmentDto(AssignmentDto dto) => dto == null
? null
: new AssignmentParameters
{
Method = dto.Method,
Preference = dto.Preference,
MinGeometryLength = dto.MinGeometryLength,
};
}
+3
View File
@@ -66,6 +66,7 @@ namespace OpenNest.IO
public double PartSpacing { get; init; }
public SpacingDto EdgeSpacing { get; init; } = new();
public double GrainAngle { get; init; }
public CuttingParametersDto CuttingParameters { get; init; }
public List<PartDto> Parts { get; init; } = new();
public List<CutOffDto> CutOffs { get; init; } = new();
}
@@ -78,6 +79,8 @@ namespace OpenNest.IO
public double Rotation { get; init; }
public bool HasManualLeadIns { get; init; }
public bool LeadInsLocked { get; init; }
public string Program { get; init; }
public string DrawingHash { get; init; }
}
public record CutOffDto
+57 -19
View File
@@ -18,6 +18,11 @@ namespace OpenNest.IO
{
private readonly Stream stream;
private readonly ZipArchive zipArchive;
private readonly Dictionary<int, string> drawingHashes = new();
private readonly List<string> warnings = new();
/// <summary>Part programs that could not be restored; other parts still load.</summary>
public IReadOnlyList<string> Warnings => warnings.AsReadOnly();
public NestReader(string file)
{
@@ -63,29 +68,59 @@ namespace OpenNest.IO
var programs = new Dictionary<int, Program>();
for (var i = 1; i <= count; i++)
{
var entry = zipArchive.GetEntry($"programs/program-{i}");
if (entry == null)
var name = $"programs/program-{i}";
if (zipArchive.GetEntry(name) == null)
continue;
using var entryStream = entry.Open();
var memStream = new MemoryStream();
entryStream.CopyTo(memStream);
memStream.Position = 0;
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);
}
programs[i] = ReadProgram(name, out var hash);
drawingHashes[i] = hash;
}
return programs;
}
private Program ReadProgram(string name, out string hash)
{
var text = ReadEntry(name);
var subs = zipArchive.GetEntry(name + "-subs") == null ? "" : ReadEntry(name + "-subs");
hash = NestWriter.GetDrawingHash(text, subs);
using var programStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
var program = new ProgramReader(programStream).Read();
if (subs.Length > 0)
{
using var subsStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(subs));
ReadSubPrograms(program, subsStream);
}
return program;
}
private void RestorePartProgram(Part part, PartDto dto, int plateId, int partIndex)
{
// Old files had only transient flags. They still load clean and silently.
if (string.IsNullOrEmpty(dto.Program))
return;
// A missing hash cannot establish that this program belongs to the current drawing.
if (!drawingHashes.TryGetValue(dto.DrawingId, out var hash)
|| !string.Equals(dto.DrawingHash, hash, StringComparison.Ordinal))
return;
try
{
var program = ReadProgram(dto.Program, out _);
foreach (var call in program.Codes.OfType<SubProgramCall>())
if (call.Program == null || !call.Program.Codes.Any(c => c is Motion))
throw new InvalidDataException($"Missing or empty hole sub-program {call.Id}.");
if (!part.RestoreLeadInProgram(program, dto.LeadInsLocked))
throw new InvalidDataException("The saved part program has no motion.");
}
catch (Exception ex) when (ex is IOException || ex is InvalidDataException || ex is FormatException
|| ex is OverflowException || ex is ArgumentException || ex is InvalidOperationException)
{
warnings.Add($"Plate {plateId}, part {partIndex} ('{part.BaseDrawing.Name}'): "
+ $"could not restore '{dto.Program}'; loaded the clean drawing without lead-ins or tabs. {ex.Message}");
}
}
private static void ReadSubPrograms(Program parent, Stream stream)
{
using var reader = new StreamReader(stream);
@@ -357,15 +392,18 @@ namespace OpenNest.IO
p.EdgeSpacing.Top
);
plate.GrainAngle = p.GrainAngle;
plate.CuttingParameters = CuttingParametersSerializer.FromDto(p.CuttingParameters);
foreach (var partDto in p.Parts)
for (var partIndex = 0; partIndex < p.Parts.Count; partIndex++)
{
var partDto = p.Parts[partIndex];
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
continue;
var part = new Part(dwg);
part.Rotate(partDto.Rotation);
part.Offset(new Vector(partDto.X, partDto.Y));
RestorePartProgram(part, partDto, p.Id, partIndex);
plate.Parts.Add(part);
}
+61 -43
View File
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using OpenNest.CNC;
@@ -18,6 +20,8 @@ namespace OpenNest.IO
private readonly Nest nest;
private Dictionary<int, Drawing> drawingDict;
private readonly Dictionary<int, string> drawingHashes = new();
private readonly Dictionary<string, Program> partPrograms = new();
public NestWriter(Nest nest)
{
@@ -38,8 +42,12 @@ namespace OpenNest.IO
using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
WriteNestJson(zipArchive);
drawingHashes.Clear();
partPrograms.Clear();
WritePrograms(zipArchive);
WriteNestJson(zipArchive);
foreach (var entry in partPrograms)
WriteProgramEntry(zipArchive, entry.Key, entry.Value);
WriteEntities(zipArchive);
WriteBestFits(zipArchive);
@@ -48,6 +56,7 @@ namespace OpenNest.IO
private void SetDrawingIds()
{
drawingDict.Clear();
var id = 1;
foreach (var drawing in nest.Drawings)
{
@@ -207,6 +216,11 @@ namespace OpenNest.IO
var match = drawingDict
.Where(dwg => dwg.Value == part.BaseDrawing)
.FirstOrDefault();
var programName = part.HasManualLeadIns
? $"parts/plate-{id}/part-{parts.Count}"
: null;
if (programName != null)
partPrograms.Add(programName, part.Program);
parts.Add(
new PartDto
{
@@ -216,6 +230,8 @@ namespace OpenNest.IO
Rotation = part.Rotation,
HasManualLeadIns = part.HasManualLeadIns,
LeadInsLocked = part.LeadInsLocked,
Program = programName,
DrawingHash = programName == null ? null : drawingHashes[match.Key],
}
);
}
@@ -264,6 +280,7 @@ namespace OpenNest.IO
Parts = parts,
CutOffs = cutoffs,
GrainAngle = plate.GrainAngle,
CuttingParameters = CuttingParametersSerializer.ToDto(plate.CuttingParameters),
}
);
}
@@ -342,43 +359,44 @@ namespace OpenNest.IO
private void WritePrograms(ZipArchive zipArchive)
{
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var name = $"programs/program-{kvp.Key}";
var stream = new MemoryStream();
WriteDrawing(stream, kvp.Value);
var entry = zipArchive.CreateEntry(name);
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);
}
drawingHashes[kvp.Key] = WriteProgramEntry(
zipArchive, $"programs/program-{kvp.Key}", kvp.Value.Program);
}
private void WriteSubPrograms(
ZipArchive zipArchive,
int drawingId,
Dictionary<int, Program> subPrograms
)
private static string WriteProgramEntry(ZipArchive zipArchive, string name, Program program)
{
var entry = zipArchive.CreateEntry($"programs/program-{drawingId}-subs");
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream, Encoding.UTF8);
var text = GetProgramText(program);
var subs = GetSubProgramsText(program);
WriteTextEntry(zipArchive, name, text);
if (subs.Length > 0)
WriteTextEntry(zipArchive, name + "-subs", subs);
return GetDrawingHash(text, subs);
}
foreach (var kvp in subPrograms.OrderBy(k => k.Key))
private static void WriteTextEntry(ZipArchive zipArchive, string name, string text)
{
var entry = zipArchive.CreateEntry(name);
using var stream = entry.Open();
using var writer = new StreamWriter(stream, new UTF8Encoding(false));
writer.Write(text);
}
// Hash the exact saved text, including holes: a change to either invalidates the part.
internal static string GetDrawingHash(string text, string subs) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(
text.Length.ToString(CultureInfo.InvariantCulture) + ":" + text + subs)));
/// <summary>Serializes the hole programs using the same text as the nest archive.</summary>
public static string GetSubProgramsText(Program program)
{
using var writer = new StringWriter(CultureInfo.InvariantCulture) { NewLine = "\n" };
foreach (var kvp in program.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));
WriteProgram(writer, kvp.Value);
writer.WriteLine("M99");
}
return writer.ToString();
}
private void WriteEntities(ZipArchive zipArchive)
@@ -402,11 +420,16 @@ namespace OpenNest.IO
}
}
private void WriteDrawing(Stream stream, Drawing drawing)
/// <summary>Serializes a program in its current local frame, without transforming it.</summary>
public static string GetProgramText(Program program)
{
using var writer = new StringWriter(CultureInfo.InvariantCulture) { NewLine = "\n" };
WriteProgram(writer, program);
return writer.ToString();
}
private static void WriteProgram(TextWriter writer, Program program)
{
var program = drawing.Program;
var writer = new StreamWriter(stream);
writer.AutoFlush = true;
// Emit variable definitions before G-code
foreach (var v in program.Variables.Values)
@@ -421,16 +444,11 @@ namespace OpenNest.IO
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
for (var i = 0; i < drawing.Program.Length; ++i)
{
var code = drawing.Program[i];
foreach (var code in program.Codes)
writer.WriteLine(GetCodeString(code));
}
stream.Position = 0;
}
private string FormatCoord(
private static string FormatCoord(
double value,
string axis,
Dictionary<string, string> variableRefs
@@ -441,7 +459,7 @@ namespace OpenNest.IO
return System.Math.Round(value, OutputPrecision).ToString(CoordinateFormat);
}
private string GetCodeString(ICode code)
private static string GetCodeString(ICode code)
{
switch (code.Type)
{
@@ -545,7 +563,7 @@ namespace OpenNest.IO
return string.Empty;
}
private string GetLayerString(LayerType layer)
private static string GetLayerString(LayerType layer)
{
switch (layer)
{