Files
OpenNest/OpenNest.IO/NestWriter.cs
T
ajandClaude Opus 5.5 d29f919aa2 fix(io): save nests safely and reopen them with their cut-off settings
Reopening a nest regenerated its cut-offs with built-in defaults (no
overtravel, a different minimum segment) rather than the machine's
settings it was made with, so a nest saved, reopened and posted could
cut scrap that never separates. Plates now record the settings their
cut-offs were last generated with, and files carry them; older files
still load with the defaults.

Saving over a nest file truncated it before writing, so any failure
part way lost the only good copy, and the error reached the user as an
unhandled exception after the save path had already changed. Nest and
settings files are now written beside the target and swapped in, and a
failed save is reported with the old file left intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:51:11 -05:00

883 lines
36 KiB
C#

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.BestFit;
using OpenNest.CNC;
using OpenNest.Support;
using static OpenNest.IO.NestFormat;
namespace OpenNest.IO
{
public sealed class NestWriter
{
private const int OutputPrecision = 10;
private const string CoordinateFormat = "0.##########";
private readonly Nest nest;
private Dictionary<int, Drawing> drawingDict;
private readonly Dictionary<int, string> drawingHashes = new();
private readonly Dictionary<string, Program> partPrograms = new();
/// <summary>
/// Supplies best-fit sets to save. Without a store, no <c>bestfits/</c> entries are
/// written; they are only a cache.
/// </summary>
public IBestFitStore BestFitStore { get; init; }
/// <summary>
/// Writes the nest's own <see cref="Nest.DateLastModified"/> instead of stamping the save
/// time. For converters and importers carrying another system's date; an ordinary save
/// leaves this false so the file records when it was saved.
/// </summary>
public bool PreserveDateLastModified { get; init; }
/// <summary>
/// Snapshots only: writes every plate, empty ones included, so a restored snapshot
/// keeps the plate list and its indices. Files keep omitting empty plates.
/// </summary>
internal bool KeepEmptyPlates { get; init; }
public NestWriter(Nest nest)
{
this.drawingDict = new Dictionary<int, Drawing>();
this.nest = nest;
}
/// <summary>
/// Writes beside <paramref name="file"/> and swaps the result in, so a failed write leaves
/// the previously saved nest intact instead of truncated.
/// </summary>
public bool Write(string file)
{
var temp = file + ".tmp";
try
{
bool written;
using (var fileStream = new FileStream(temp, FileMode.Create))
written = Write(fileStream);
if (!written)
{
File.Delete(temp);
return false;
}
File.Move(temp, file, overwrite: true);
return true;
}
catch
{
try
{
File.Delete(temp);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
throw;
}
}
public bool Write(Stream stream)
{
if (!PreserveDateLastModified)
nest.DateLastModified = DateTime.Now;
SetDrawingIds();
using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
drawingHashes.Clear();
partPrograms.Clear();
WritePrograms(zipArchive);
WriteNestJson(zipArchive);
foreach (var entry in partPrograms)
WriteProgramEntry(zipArchive, entry.Key, entry.Value);
WriteEntities(zipArchive);
WriteBestFits(zipArchive);
return true;
}
private void SetDrawingIds()
{
drawingDict.Clear();
var id = 1;
foreach (var drawing in nest.Drawings)
{
drawingDict.Add(id, drawing);
id++;
}
}
private void WriteNestJson(ZipArchive zipArchive)
{
var dto = BuildNestDto();
var json = JsonSerializer.Serialize(dto, JsonOptions);
var entry = zipArchive.CreateEntry("nest.json");
using var stream = entry.Open();
using var writer = new StreamWriter(stream, Encoding.UTF8);
writer.Write(json);
}
private NestDto BuildNestDto()
{
return new NestDto
{
Version = 2,
Name = nest.Name ?? "",
Units = nest.Units.ToString(),
Customer = nest.Customer ?? "",
DateCreated = nest.DateCreated.ToString("o"),
DateLastModified = nest.DateLastModified.ToString("o"),
Notes = nest.Notes ?? "",
AssistGas = nest.AssistGas ?? "",
Status = nest.Status.ToString(),
MadeBy = nest.MadeBy ?? "",
Machine = nest.Machine is { } machine
? new MachineDto { Id = machine.Id, Name = machine.Name }
: null,
Thickness = nest.Thickness,
Material = new MaterialDto
{
Name = nest.Material.Name ?? "",
Grade = nest.Material.Grade ?? "",
Density = nest.Material.Density,
},
PlateDefaults = BuildPlateDefaultsDto(),
Drawings = BuildDrawingDtos(),
Plates = BuildPlateDtos(),
PlateOptions =
nest.PlateOptions?.Select(o => new PlateOptionDto
{
Width = o.Width,
Length = o.Length,
Cost = o.Cost,
})
.ToList()
?? new(),
SalvageRate = nest.SalvageRate,
MinimumRemnantDimension = nest.MinimumRemnantDimension,
};
}
private PlateDefaultsDto BuildPlateDefaultsDto()
{
var pd = nest.PlateDefaults;
return new PlateDefaultsDto
{
Size = new SizeDto { Width = pd.Size.Width, Length = pd.Size.Length },
Thickness = nest.Thickness,
Quadrant = pd.Quadrant,
PartSpacing = pd.PartSpacing,
Material = new MaterialDto
{
Name = nest.Material.Name ?? "",
Grade = nest.Material.Grade ?? "",
Density = nest.Material.Density,
},
EdgeSpacing = new SpacingDto
{
Left = pd.EdgeSpacing.Left,
Top = pd.EdgeSpacing.Top,
Right = pd.EdgeSpacing.Right,
Bottom = pd.EdgeSpacing.Bottom,
},
ClampRail = ToDto(pd.ClampRail),
SupportBed = ToDto(nest.SupportBed),
};
}
private static SupportBedDto ToDto(SupportBed bed) =>
bed switch
{
SupportGrid grid => new SupportBedDto
{
PitchX = grid.PitchX,
PitchY = grid.PitchY,
OffsetX = grid.OffsetX,
OffsetY = grid.OffsetY,
RegistrationTolerance = grid.RegistrationTolerance,
StabilityMargin = grid.StabilityMargin,
},
SupportSlats slats => new SupportBedDto
{
Type = "slats",
Axis = slats.Axis == SlatAxis.X ? "x" : "y",
Pitch = slats.Pitch,
Thickness = slats.Thickness,
Offset = slats.Offset,
RegistrationTolerance = slats.RegistrationTolerance,
StabilityMargin = slats.StabilityMargin,
},
_ => null,
};
private List<DrawingDto> BuildDrawingDtos()
{
var list = new List<DrawingDto>();
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var d = kvp.Value;
list.Add(
new DrawingDto
{
Id = kvp.Key,
Name = d.Name ?? "",
Customer = d.Customer ?? "",
Color = new ColorDto
{
A = d.Color.A,
R = d.Color.R,
G = d.Color.G,
B = d.Color.B,
},
CustomColor = d.HasCustomColor,
Quantity = new QuantityDto { Required = d.Quantity.Required },
Priority = d.Priority,
CutFirst = d.CutFirst,
Constraints = new ConstraintsDto
{
StepAngle = d.Constraints.StepAngle,
StartAngle = d.Constraints.StartAngle,
EndAngle = d.Constraints.EndAngle,
Allow180Equivalent = d.Constraints.Allow180Equivalent,
},
Material = new MaterialDto
{
Name = d.Material.Name ?? "",
Grade = d.Material.Grade ?? "",
Density = d.Material.Density,
},
Source = new SourceDto
{
Path = d.Source.Path ?? "",
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y },
},
Bends =
d.Bends?.Select(b => new BendDto
{
StartX = b.StartPoint.X,
StartY = b.StartPoint.Y,
EndX = b.EndPoint.X,
EndY = b.EndPoint.Y,
Direction = b.Direction.ToString(),
Angle = b.Angle,
Radius = b.Radius,
NoteText = b.NoteText ?? "",
})
.ToList()
?? new List<BendDto>(),
Library = ToLibraryLinkDto(d.Library),
CutPins = d.CutPins?.Count > 0 ? d.CutPins.Select(CutPinFormat.ToDto).ToList() : null,
Breakoffs = d.Breakoffs?.Count > 0
? d.Breakoffs.Select(b => new BreakoffDto
{
EdgeIds = b.EdgeIds.ToList(),
Flip = b.Flip,
TabWidth = b.TabWidth,
TabCenters = b.TabCenters.ToList(),
}).ToList()
: null,
TabbedCutouts = d.TabbedCutouts?.Count > 0
? d.TabbedCutouts.Select(c => new TabbedCutoutDto
{
EdgeIds = c.EdgeIds.ToList(),
TabWidth = c.TabWidth,
TabCenters = c.TabCenters.ToList(),
}).ToList()
: null,
Dimensions = d.Dimensions?.Count > 0
? d.Dimensions.Select(m => new DimensionDto
{
Kind = m.Kind.ToString(),
Offset = m.Offset,
EntityId = m.IsRadial ? m.EntityId : null,
Angle = m.IsRadial ? m.Angle : null,
}).ToList()
: null,
}
);
}
return list;
}
private static DrawingLibraryLinkDto ToLibraryLinkDto(DrawingLibraryLink link) =>
link == null
? null
: new DrawingLibraryLinkDto
{
ServerKey = link.ServerKey,
Id = link.Id,
Revision = link.Revision,
CutType = link.CutType.ToString(),
Thickness = link.Thickness,
MaterialName = link.MaterialName,
MaterialGrade = link.MaterialGrade,
Units = link.Units.ToString(),
Notes = link.Notes,
PackageSha256 = link.PackageSha256,
};
/// <summary>
/// True when a plate created from the saved defaults would be this sheet's clamp
/// configuration: same size, quadrant, edge and part spacing, no last-resort
/// removal, not following another rail (removal time included), and zones the
/// saved rail recreates (C17: anything else is configured
/// stock and is written).
/// </summary>
private bool DefaultsRecreateClamps(Plate plate)
{
var defaults = nest.PlateDefaults;
return defaults.ClampRail is { } rail
&& plate.RemovedClamps.Count == 0
&& plate.Size.Width == defaults.Size.Width
&& plate.Size.Length == defaults.Size.Length
&& plate.Quadrant == defaults.Quadrant
&& plate.PartSpacing == defaults.PartSpacing
&& plate.EdgeSpacing.Left == defaults.EdgeSpacing.Left
&& plate.EdgeSpacing.Bottom == defaults.EdgeSpacing.Bottom
&& plate.EdgeSpacing.Right == defaults.EdgeSpacing.Right
&& plate.EdgeSpacing.Top == defaults.EdgeSpacing.Top
&& (plate.ClampRail == null || Equals(plate.ClampRail, rail))
&& rail.Matches(plate);
}
private static ClampRailDto ToDto(ClampRail rail) =>
rail == null
? null
: new ClampRailDto
{
Width = rail.Width,
Depth = rail.Depth,
Centers = rail.Centers.ToList(),
FarSideCenters = CentersOn(rail, ClampEdge.FarSide),
OriginEndCenters = CentersOn(rail, ClampEdge.OriginEnd),
FarEndCenters = CentersOn(rail, ClampEdge.FarEnd),
RemovalMinutes = rail.RemovalMinutes,
Clearance = rail.Clearance,
};
private static List<double> CentersOn(ClampRail rail, ClampEdge edge) =>
rail.Clamps.Where(c => c.Edge == edge).Select(c => c.Center).ToList();
/// <summary>Numeric suffix of a rail clamp id ("clamp-3" -&gt; 3), for a natural sort of
/// <see cref="PlateDto.RemovedClamps"/> instead of lexical ("clamp-10" before "clamp-2").
/// A non-matching id sorts last by its own ordinal text.</summary>
private static int ClampIndex(string id) =>
id != null && id.StartsWith("clamp-", StringComparison.Ordinal)
&& int.TryParse(id.AsSpan(6), out var index)
? index
: int.MaxValue;
private List<PlateDto> BuildPlateDtos()
{
var list = new List<PlateDto>();
var id = 0;
for (var i = 0; i < nest.Plates.Count; i++)
{
var plate = nest.Plates[i];
// An empty plate is omitted unless that would lose a clamp configuration the
// saved defaults cannot recreate. PlateManager moves an untouched trailing
// sentinel to new defaults during an edit, so it remains omitted; configured
// nondefault clamp sheets are still written.
if (
!KeepEmptyPlates
&& plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0
&& plate.CutOffs.Count == 0
&& plate.Outline == null
&& (
(plate.ClampZones.Count == 0 && plate.RemovedClamps.Count == 0)
|| DefaultsRecreateClamps(plate)
)
)
continue;
id++;
var parts = new List<PartDto>();
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
{
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
{
DrawingId = match.Key,
X = part.Location.X,
Y = part.Location.Y,
Rotation = part.Rotation,
HasManualLeadIns = part.HasManualLeadIns,
LeadInsLocked = part.LeadInsLocked,
Program = programName,
DrawingHash = programName == null ? null : drawingHashes[match.Key],
CuttingParameters = programName == null
? null
: CuttingParametersSerializer.ToDto(part.CuttingParameters),
DrawingPinsOff = part.DrawingPinsOff ? true : null,
}
);
}
var cutoffs = new List<CutOffDto>();
foreach (var cutoff in plate.CutOffs)
{
// Each trimmed segment is its own part with its own place in the sequence.
// Every place is written, so a reader regenerating a different number of
// segments still knows how many places the saved ones held.
var segments = CutOffSegments.Of(plate, cutoff)
.Select(segment => plate.Parts.IndexOf(segment.Part))
.Where(index => index >= 0)
.ToArray();
cutoffs.Add(
new CutOffDto
{
X = cutoff.Position.X,
Y = cutoff.Position.Y,
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
StartLimit = cutoff.StartLimit,
EndLimit = cutoff.EndLimit,
Sequence = segments.Length > 0 ? segments.Min() : null,
Segments = segments.Length > 0 ? segments : null,
HeadLockOvercut = cutoff.HeadLockOvercut,
FarEdgeFirst = cutoff.FarEdgeFirst,
Gaps = cutoff.Gaps.Count == 0 ? null
: cutoff.Gaps.Select(gap => new CutOffGapDto { Start = gap.Start, End = gap.End }).ToArray(),
}
);
}
list.Add(
new PlateDto
{
Id = id,
Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length },
Quadrant = plate.Quadrant,
Quantity = plate.Quantity,
PartSpacing = plate.PartSpacing,
EdgeSpacing = new SpacingDto
{
Left = plate.EdgeSpacing.Left,
Top = plate.EdgeSpacing.Top,
Right = plate.EdgeSpacing.Right,
Bottom = plate.EdgeSpacing.Bottom,
},
Parts = parts,
CutOffs = cutoffs,
CutOffSettings = cutoffs.Count == 0 || plate.CutOffSettings is not { } settings ? null
: new CutOffSettingsDto
{
PartClearance = settings.PartClearance,
Overtravel = settings.Overtravel,
MinSegmentLength = settings.MinSegmentLength,
CutDirection = settings.CutDirection == CutDirection.TowardOrigin ? "towardOrigin" : "awayFromOrigin",
LockHeadHeight = settings.LockHeadHeight,
HeadLockOvercut = settings.HeadLockOvercut,
},
GrainAngle = plate.GrainAngle,
CuttingParameters = CuttingParametersSerializer.ToDto(plate.CuttingParameters),
Clamps = plate.ClampZones
.Select(z => new ClampZoneDto
{
Id = z.Id,
X = z.Bounds.X,
Y = z.Bounds.Y,
Length = z.Bounds.Length,
Width = z.Bounds.Width,
})
.ToList(),
// Omitted unless a clamp was removed (files without the feature
// stay unchanged).
RemovedClamps = plate.RemovedClamps.Count == 0
? null
: plate.RemovedClamps
.OrderBy(ClampIndex)
.ThenBy(id => id, StringComparer.Ordinal)
.ToList(),
// Only a rail other than the nest default needs saving per sheet.
ClampRail = Equals(plate.ClampRail, nest.PlateDefaults.ClampRail)
? null
: ToDto(plate.ClampRail),
StockItemCodes = plate.StockItemCodes.Count == 0 ? null : plate.StockItemCodes.ToList(),
ExpectedRemnant = ToDto(PlateRemnant.Expected(plate, nest.MinimumRemnantDimension)),
Outline = ToDto(plate.Outline),
}
);
}
return list;
}
private static SheetOutlineDto ToDto(SheetOutline outline) =>
outline == null
? null
: new SheetOutlineDto
{
Points = outline.Points.Select(p => new PointDto { X = p.X, Y = p.Y }).ToList(),
Obstacles = outline.Obstacles.Count == 0
? null
: outline.Obstacles
.Select(o => o.Select(p => new PointDto { X = p.X, Y = p.Y }).ToList())
.ToList(),
};
private static ExpectedRemnantDto ToDto(PlateRemnant remnant) =>
remnant == null
? null
: new ExpectedRemnantDto
{
Width = System.Math.Min(remnant.Box.Width, remnant.Box.Length),
Length = System.Math.Max(remnant.Box.Width, remnant.Box.Length),
Source = remnant.Source == PlateRemnantSource.CutOff ? "cutoff" : "offcut",
};
private void WriteBestFits(ZipArchive zipArchive)
{
if (BestFitStore == null)
return;
// Only save best-fit sets for plate sizes actually used in this nest.
var plateKeys = new HashSet<(double Width, double Length, double Spacing)>();
foreach (var plate in nest.Plates)
plateKeys.Add((plate.Size.Width, plate.Size.Length, plate.PartSpacing));
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var sets = BestFitStore.Export(kvp.Value, plateKeys).Select(ToDto).ToList();
if (sets.Count == 0)
continue;
var json = JsonSerializer.Serialize(sets, JsonOptions);
var entry = zipArchive.CreateEntry($"bestfits/bestfit-{kvp.Key}");
using var stream = entry.Open();
using var writer = new StreamWriter(stream, Encoding.UTF8);
writer.Write(json);
}
}
private static BestFitSetDto ToDto(SavedPairSet set) =>
new()
{
PlateWidth = set.PlateWidth,
PlateHeight = set.PlateHeight,
Spacing = set.Spacing,
CanonicalAngle = set.CanonicalAngle,
Results = set
.Results.Select(r => new BestFitResultDto
{
Part1Rotation = r.Part1Rotation,
Part2Rotation = r.Part2Rotation,
Part2OffsetX = r.Part2OffsetX,
Part2OffsetY = r.Part2OffsetY,
StrategyType = r.StrategyType,
TestNumber = r.TestNumber,
CandidateSpacing = r.CandidateSpacing,
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
Keep = r.Keep,
Reason = r.Reason ?? "",
TrueArea = r.TrueArea,
HullAngles = r.HullAngles?.ToList() ?? new List<double>(),
})
.ToList(),
};
private void WritePrograms(ZipArchive zipArchive)
{
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
drawingHashes[kvp.Key] = WriteProgramEntry(
zipArchive, $"programs/program-{kvp.Key}", kvp.Value.Program);
}
private static string WriteProgramEntry(ZipArchive zipArchive, string name, Program program)
{
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);
}
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}");
WriteProgram(writer, kvp.Value);
writer.WriteLine("M99");
}
return writer.ToString();
}
private void WriteEntities(ZipArchive zipArchive)
{
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var json = GetEntitiesJson(kvp.Value);
if (json == null)
continue;
var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}");
using var stream = entry.Open();
using var writer = new StreamWriter(stream, Encoding.UTF8);
writer.Write(json);
}
}
private static string GetEntitiesJson(Drawing drawing)
{
if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0)
return null;
var dto = EntitySerializer.ToDto(
drawing.SourceEntities,
drawing.SuppressedEntityIds,
drawing.EntityRoles
);
return JsonSerializer.Serialize(dto, JsonOptions);
}
/// <summary>
/// One fingerprint per drawing of <paramref name="nest"/>, in order, over everything a
/// save writes for it: its <c>nest.json</c> entry (without the archive id), program,
/// hole sub-programs and source entities. Equal fingerprints mean equal saved content.
/// </summary>
internal static List<string> GetDrawingFingerprints(Nest nest)
{
var writer = new NestWriter(nest);
writer.SetDrawingIds();
var dtos = writer.BuildDrawingDtos();
var result = new List<string>(dtos.Count);
foreach (var dto in dtos)
{
var drawing = writer.drawingDict[dto.Id];
var node = System.Text.Json.Nodes.JsonNode.Parse(JsonSerializer.Serialize(dto, JsonOptions));
result.Add(GetDrawingFingerprint(
node.AsObject(),
GetProgramText(drawing.Program),
GetSubProgramsText(drawing.Program),
GetEntitiesJson(drawing)));
}
return result;
}
/// <summary>
/// Hashes a drawing's saved content. <paramref name="drawingJson"/> is its
/// <c>nest.json</c> entry as parsed from serializer output; its <c>id</c> is removed.
/// </summary>
internal static string GetDrawingFingerprint(System.Text.Json.Nodes.JsonObject drawingJson,
string programText, string subsText, string entitiesJson)
{
drawingJson.Remove("id");
using var sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
foreach (var part in new[] { drawingJson.ToJsonString(), programText, subsText, entitiesJson })
{
// Length-prefixed, so no part can run into the next one.
var text = part ?? "";
sha.AppendData(Encoding.UTF8.GetBytes(
(part == null ? "-" : text.Length.ToString(CultureInfo.InvariantCulture)) + ":" + text));
}
return Convert.ToHexString(sha.GetHashAndReset());
}
/// <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)
{
// Emit variable definitions before G-code
foreach (var v in program.Variables.Values)
{
var line = $"{v.Name} = {v.Expression}";
if (v.Inline)
line += " inline";
if (v.Global)
line += " global";
writer.WriteLine(line);
}
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
foreach (var code in program.Codes)
writer.WriteLine(GetCodeString(code));
}
private static string FormatCoord(
double value,
string axis,
Dictionary<string, string> variableRefs
)
{
if (variableRefs != null && variableRefs.TryGetValue(axis, out var varName))
return $"${varName}";
return System.Math.Round(value, OutputPrecision).ToString(CoordinateFormat);
}
private static string GetCodeString(ICode code)
{
switch (code.Type)
{
case CodeType.ArcMove:
{
var sb = new StringBuilder();
var arcMove = (ArcMove)code;
var refs = arcMove.VariableRefs;
var x = FormatCoord(arcMove.EndPoint.X, "X", refs);
var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs);
var i = FormatCoord(arcMove.CenterPoint.X, "I", refs);
var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs);
sb.Append(
arcMove.Rotation == RotationType.CW
? $"G02X{x}Y{y}I{i}J{j}"
: $"G03X{x}Y{y}I{i}J{j}"
);
if (arcMove.Layer != LayerType.Cut)
sb.Append(GetLayerString(arcMove.Layer));
if (arcMove.Suppressed)
sb.Append(":SUPPRESSED");
return sb.ToString();
}
case CodeType.Comment:
{
var comment = (Comment)code;
return ":" + comment.Value;
}
case CodeType.LinearMove:
{
var sb = new StringBuilder();
var linearMove = (LinearMove)code;
var refs = linearMove.VariableRefs;
sb.Append(
$"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}"
);
if (linearMove.Layer != LayerType.Cut)
sb.Append(GetLayerString(linearMove.Layer));
if (linearMove.Suppressed)
sb.Append(":SUPPRESSED");
return sb.ToString();
}
case CodeType.RapidMove:
{
var rapidMove = (RapidMove)code;
var refs = rapidMove.VariableRefs;
return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}";
}
case CodeType.SetFeedrate:
{
var setFeedrate = (Feedrate)code;
if (setFeedrate.VariableRef != null)
return $"F${setFeedrate.VariableRef}";
return "F" + setFeedrate.Value;
}
case CodeType.SetKerf:
{
var setKerf = (Kerf)code;
switch (setKerf.Value)
{
case KerfType.None:
return "G40";
case KerfType.Left:
return "G41";
case KerfType.Right:
return "G42";
}
break;
}
case CodeType.SubProgramCall:
{
var subProgramCall = (SubProgramCall)code;
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}";
}
}
return string.Empty;
}
private static string GetLayerString(LayerType layer)
{
switch (layer)
{
case LayerType.Display:
return ":DISPLAY";
case LayerType.Leadin:
return ":LEADIN";
case LayerType.Leadout:
return ":LEADOUT";
case LayerType.Scribe:
return ":SCRIBE";
case LayerType.Break:
return ":BREAK";
case LayerType.Tab:
return ":TAB";
case LayerType.DisplayPerimeter:
return ":DISPLAYPERIMETER";
default:
return string.Empty;
}
}
}
}