feat(io): persist part cutting programs and plate parameters
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user