mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-30 13:01:07 -04:00
feat(io): add in-memory nest snapshots for undo
Undo will keep whole-nest states and restore by reading one back. NestSnapshot.Capture writes the same archive a save writes, with three differences Phase 0 found necessary: it does not stamp DateLastModified on the live nest, it leaves best fits out (they are a cache), and it keeps empty plates, which files drop, so an undo does not shift plate indices or lose a plate the user added. File output is unchanged. Restore reuses each live drawing whose saved content is unchanged, matched by a fingerprint over its nest.json entry, program, hole sub-programs and source entities. BestFitCache and FillResultCache are keyed by the Drawing object, so a plain read would make the next fill recompute every best-fit pair; reuse keeps those caches, colours and Drawing.Id, and skips parsing the reused programs. Nested counts are recomputed for the restored nest, and given back to the live one if the read fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a88c056f4b
commit
fe31a3f8ec
4 files changed
+383
-18
No files matched your search
@@ -0,0 +1,188 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.IO.Tests;
|
||||
|
||||
public class NestSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void Restore_KeepsEmptyPlatesInPlace_AndEveryPlacement()
|
||||
{
|
||||
var live = Fixture(out var parameters);
|
||||
|
||||
var restored = NestSnapshot.Capture(live).Restore(live);
|
||||
|
||||
Assert.Equal(live.Plates.Count, restored.Plates.Count);
|
||||
Assert.Equal(new[] { 3, 0, 1, 0 }, restored.Plates.Select(p => p.Parts.Count(x => !x.BaseDrawing.IsCutOff)));
|
||||
for (var i = 0; i < live.Plates.Count; i++)
|
||||
{
|
||||
var expected = live.Plates[i];
|
||||
var actual = restored.Plates[i];
|
||||
Assert.Equal(expected.Size.Width, actual.Size.Width);
|
||||
Assert.Equal(expected.Size.Length, actual.Size.Length);
|
||||
Assert.Equal(expected.CutOffs.Count, actual.CutOffs.Count);
|
||||
Assert.Equal(expected.Parts.Count, actual.Parts.Count);
|
||||
for (var k = 0; k < expected.Parts.Count; k++)
|
||||
{
|
||||
Assert.Equal(expected.Parts[k].Location.X, actual.Parts[k].Location.X, 9);
|
||||
Assert.Equal(expected.Parts[k].Location.Y, actual.Parts[k].Location.Y, 9);
|
||||
Assert.Equal(expected.Parts[k].Rotation, actual.Parts[k].Rotation, 9);
|
||||
Assert.Equal(expected.Parts[k].HasManualLeadIns, actual.Parts[k].HasManualLeadIns);
|
||||
Assert.Equal(expected.Parts[k].LeadInsLocked, actual.Parts[k].LeadInsLocked);
|
||||
}
|
||||
}
|
||||
|
||||
var leaded = restored.Plates[0].Parts[0];
|
||||
Assert.Equal(CuttingParametersSerializer.Serialize(parameters),
|
||||
CuttingParametersSerializer.Serialize(leaded.CuttingParameters));
|
||||
Assert.Null(restored.Plates[0].Parts[1].CuttingParameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Files_StillOmitEmptyPlates()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
new NestWriter(live).Write(stream);
|
||||
var read = new NestReader(new MemoryStream(stream.ToArray())).Read();
|
||||
|
||||
Assert.Equal(2, read.Plates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_LeavesTheModifiedDateAlone()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
var date = new DateTime(2024, 3, 4, 5, 6, 7);
|
||||
live.DateLastModified = date;
|
||||
|
||||
var snapshot = NestSnapshot.Capture(live);
|
||||
|
||||
Assert.Equal(date, live.DateLastModified);
|
||||
Assert.Equal(date, snapshot.Restore(live).DateLastModified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_ReusesUnchangedDrawings_AndReplacesChangedOnes()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
var square = live.Drawings.ElementAt(0);
|
||||
var bar = live.Drawings.ElementAt(1);
|
||||
var snapshot = NestSnapshot.Capture(live);
|
||||
bar.Quantity.Required = 99; // Changed since the copy: the copy's bar comes back.
|
||||
|
||||
var restored = snapshot.Restore(live);
|
||||
|
||||
Assert.Same(square, restored.Drawings.ElementAt(0));
|
||||
Assert.NotSame(bar, restored.Drawings.ElementAt(1));
|
||||
Assert.Equal(5, restored.Drawings.ElementAt(1).Quantity.Required);
|
||||
var drawings = restored.Drawings.ToArray();
|
||||
Assert.All(restored.Plates.SelectMany(p => p.Parts).Where(p => !p.BaseDrawing.IsCutOff),
|
||||
part => Assert.Contains(drawings, d => ReferenceEquals(d, part.BaseDrawing)));
|
||||
Assert.Equal(2, square.Quantity.Nested); // Counted once, from the restored nest.
|
||||
Assert.Equal(2, drawings[1].Quantity.Nested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_ReplacesADrawingWhoseProgramChanged()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
var square = live.Drawings.ElementAt(0);
|
||||
var snapshot = NestSnapshot.Capture(live);
|
||||
var before = NestWriter.GetProgramText(square.Program);
|
||||
square.Program = ConvertGeometry.ToProgram(new List<Entity>
|
||||
{
|
||||
new Line(0, 0, 5, 0),
|
||||
new Line(5, 0, 5, 5),
|
||||
new Line(5, 5, 0, 5),
|
||||
new Line(0, 5, 0, 0),
|
||||
});
|
||||
|
||||
var restored = snapshot.Restore(live).Drawings.ElementAt(0);
|
||||
|
||||
Assert.NotSame(square, restored);
|
||||
Assert.Equal(before, NestWriter.GetProgramText(restored.Program));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_WithoutALiveNest_ReadsFreshDrawings()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
|
||||
var restored = NestSnapshot.Capture(live).Restore();
|
||||
|
||||
Assert.DoesNotContain(restored.Drawings, d => live.Drawings.Any(l => ReferenceEquals(l, d)));
|
||||
Assert.Equal(live.Drawings.Select(d => d.Name), restored.Drawings.Select(d => d.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_KeepsLeadInProgramsOnReusedDrawings()
|
||||
{
|
||||
var live = Fixture(out _);
|
||||
var expected = NestWriter.GetProgramText(live.Plates[0].Parts[0].Program);
|
||||
|
||||
var restored = NestSnapshot.Capture(live).Restore(live);
|
||||
|
||||
var part = restored.Plates[0].Parts[0];
|
||||
Assert.Same(live.Drawings.ElementAt(0), part.BaseDrawing);
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
Assert.True(part.LeadInsLocked);
|
||||
Assert.Equal(expected, NestWriter.GetProgramText(part.Program));
|
||||
}
|
||||
|
||||
// Plates: [3 parts, one with locked manual leads] [empty] [1 part + a cut-off] [empty].
|
||||
private static Nest Fixture(out CuttingParameters parameters)
|
||||
{
|
||||
var nest = new Nest("snapshot");
|
||||
nest.PlateDefaults.Size = new Size(40, 20);
|
||||
var square = Square();
|
||||
var bar = new Drawing("Bar", ConvertGeometry.ToProgram(new List<Entity>
|
||||
{
|
||||
new Line(0, 0, 8, 0),
|
||||
new Line(8, 0, 8, 2),
|
||||
new Line(8, 2, 0, 2),
|
||||
new Line(0, 2, 0, 0),
|
||||
}));
|
||||
bar.Quantity.Required = 5;
|
||||
nest.Drawings.Add(square);
|
||||
nest.Drawings.Add(bar);
|
||||
|
||||
var first = nest.CreatePlate();
|
||||
parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
ExternalLeadOut = new NoLeadOut(),
|
||||
InternalLeadOut = new NoLeadOut(),
|
||||
};
|
||||
var leaded = new Part(square, new Vector(1, 1));
|
||||
leaded.ApplyLeadIns(parameters, Vector.Zero);
|
||||
leaded.LeadInsLocked = true;
|
||||
first.Parts.Add(leaded);
|
||||
first.Parts.Add(new Part(square, new Vector(6, 1)));
|
||||
var rotated = new Part(bar, new Vector(12, 1));
|
||||
rotated.Rotate(System.Math.PI / 2);
|
||||
first.Parts.Add(rotated);
|
||||
|
||||
nest.CreatePlate();
|
||||
|
||||
var third = nest.CreatePlate();
|
||||
third.Parts.Add(new Part(bar, new Vector(1, 1)));
|
||||
third.CutOffs.Add(new CutOff(new Vector(20, 0), CutOffAxis.Vertical));
|
||||
third.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
nest.CreatePlate();
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Drawing Square() =>
|
||||
new("Square", ConvertGeometry.ToProgram(new List<Entity>
|
||||
{
|
||||
new Line(0, 0, 4, 0),
|
||||
new Line(4, 0, 4, 4),
|
||||
new Line(4, 4, 0, 4),
|
||||
new Line(0, 4, 0, 0),
|
||||
}));
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.BestFit;
|
||||
using OpenNest.Breakoffs;
|
||||
@@ -34,6 +35,12 @@ namespace OpenNest.IO
|
||||
/// </summary>
|
||||
public IBestFitStore BestFitStore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot restore only: live drawings by <see cref="NestWriter.GetDrawingFingerprints"/>,
|
||||
/// used in place of saved drawings with the same fingerprint.
|
||||
/// </summary>
|
||||
internal IReadOnlyDictionary<string, Drawing> ReusableDrawings { get; init; }
|
||||
|
||||
public NestReader(string file)
|
||||
{
|
||||
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
||||
@@ -51,9 +58,10 @@ namespace OpenNest.IO
|
||||
var nestJson = ReadEntry("nest.json");
|
||||
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||
|
||||
var programs = ReadPrograms(dto.Drawings.Count);
|
||||
var entitySets = ReadEntitySets(dto.Drawings.Count);
|
||||
var drawingMap = BuildDrawings(dto, programs, entitySets);
|
||||
var reused = MatchReusableDrawings(nestJson);
|
||||
var programs = ReadPrograms(dto.Drawings.Count, reused);
|
||||
var entitySets = ReadEntitySets(dto.Drawings.Count, reused);
|
||||
var drawingMap = BuildDrawings(dto, programs, entitySets, reused);
|
||||
ReadBestFits(drawingMap);
|
||||
var nest = BuildNest(dto, drawingMap);
|
||||
// Nests converted from PEP kept etch marks only in the placed programs; give them
|
||||
@@ -76,13 +84,50 @@ namespace OpenNest.IO
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
private Dictionary<int, Program> ReadPrograms(int count)
|
||||
/// <summary>
|
||||
/// Matches saved drawings to <see cref="ReusableDrawings"/> by fingerprint. A matched
|
||||
/// drawing is used as it is: its program and entities are not parsed, though its
|
||||
/// program hash is still recorded so saved part programs restore against it.
|
||||
/// </summary>
|
||||
private Dictionary<int, Drawing> MatchReusableDrawings(string nestJson)
|
||||
{
|
||||
var reused = new Dictionary<int, Drawing>();
|
||||
if (ReusableDrawings == null || ReusableDrawings.Count == 0)
|
||||
return reused;
|
||||
|
||||
var drawings = JsonNode.Parse(nestJson)?["drawings"]?.AsArray();
|
||||
if (drawings == null)
|
||||
return reused;
|
||||
|
||||
foreach (var node in drawings)
|
||||
{
|
||||
var json = node.AsObject();
|
||||
var id = json["id"].GetValue<int>();
|
||||
var name = $"programs/program-{id}";
|
||||
if (zipArchive.GetEntry(name) == null)
|
||||
continue;
|
||||
|
||||
var text = ReadEntry(name);
|
||||
var subs = zipArchive.GetEntry(name + "-subs") == null ? "" : ReadEntry(name + "-subs");
|
||||
var entitiesName = $"entities/entities-{id}";
|
||||
var entities = zipArchive.GetEntry(entitiesName) == null ? null : ReadEntry(entitiesName);
|
||||
var fingerprint = NestWriter.GetDrawingFingerprint(json, text, subs, entities);
|
||||
if (ReusableDrawings.TryGetValue(fingerprint, out var live))
|
||||
{
|
||||
reused[id] = live;
|
||||
drawingHashes[id] = NestWriter.GetDrawingHash(text, subs);
|
||||
}
|
||||
}
|
||||
return reused;
|
||||
}
|
||||
|
||||
private Dictionary<int, Program> ReadPrograms(int count, Dictionary<int, Drawing> reused)
|
||||
{
|
||||
var programs = new Dictionary<int, Program>();
|
||||
for (var i = 1; i <= count; i++)
|
||||
{
|
||||
var name = $"programs/program-{i}";
|
||||
if (zipArchive.GetEntry(name) == null)
|
||||
if (reused.ContainsKey(i) || zipArchive.GetEntry(name) == null)
|
||||
continue;
|
||||
programs[i] = ReadProgram(name, out var hash);
|
||||
drawingHashes[i] = hash;
|
||||
@@ -190,14 +235,15 @@ namespace OpenNest.IO
|
||||
}
|
||||
|
||||
private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed, Dictionary<Guid, EntityRole> roles)> ReadEntitySets(
|
||||
int count
|
||||
int count,
|
||||
Dictionary<int, Drawing> reused
|
||||
)
|
||||
{
|
||||
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>, Dictionary<Guid, EntityRole>)>();
|
||||
for (var i = 1; i <= count; i++)
|
||||
{
|
||||
var entry = zipArchive.GetEntry($"entities/entities-{i}");
|
||||
if (entry == null)
|
||||
if (entry == null || reused.ContainsKey(i))
|
||||
continue;
|
||||
|
||||
using var entryStream = entry.Open();
|
||||
@@ -213,12 +259,19 @@ namespace OpenNest.IO
|
||||
private Dictionary<int, Drawing> BuildDrawings(
|
||||
NestDto dto,
|
||||
Dictionary<int, Program> programs,
|
||||
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed, Dictionary<Guid, EntityRole> roles)> entitySets
|
||||
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed, Dictionary<Guid, EntityRole> roles)> entitySets,
|
||||
Dictionary<int, Drawing> reused
|
||||
)
|
||||
{
|
||||
var map = new Dictionary<int, Drawing>();
|
||||
foreach (var d in dto.Drawings)
|
||||
{
|
||||
if (reused.TryGetValue(d.Id, out var live))
|
||||
{
|
||||
map[d.Id] = live;
|
||||
continue;
|
||||
}
|
||||
|
||||
var drawing = new Drawing(d.Name);
|
||||
drawing.Customer = d.Customer;
|
||||
drawing.Color = Color.FromArgb(d.Color.A, d.Color.R, d.Color.G, d.Color.B);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// An in-memory copy of a whole nest for undo: the <c>.nest</c> archive a save would
|
||||
/// write, except that empty plates are kept, best fits are left out (they are a cache)
|
||||
/// and the nest's modified date is not stamped.
|
||||
/// </summary>
|
||||
public sealed class NestSnapshot
|
||||
{
|
||||
private readonly byte[] bytes;
|
||||
|
||||
private NestSnapshot(byte[] bytes) => this.bytes = bytes;
|
||||
|
||||
public long SizeInBytes => bytes.Length;
|
||||
|
||||
/// <summary>The archive, for tests and diagnostics; never written over a user's file.</summary>
|
||||
public ReadOnlySpan<byte> Bytes => bytes;
|
||||
|
||||
public static NestSnapshot Capture(Nest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest) { PreserveDateLastModified = true, KeepEmptyPlates = true }.Write(stream);
|
||||
return new NestSnapshot(stream.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the copy back as a new nest. Each drawing of <paramref name="live"/> whose saved
|
||||
/// content equals a drawing in the copy is used in its place, so caches keyed by the
|
||||
/// drawing (best fits, fill results), its colour and its id survive; a drawing whose
|
||||
/// content changed comes from the copy. Reused drawings' nested counts then follow the
|
||||
/// returned nest, which replaces <paramref name="live"/>.
|
||||
/// </summary>
|
||||
public Nest Restore(Nest live = null)
|
||||
{
|
||||
var reusable = live == null ? null : ReusableDrawings(live);
|
||||
try
|
||||
{
|
||||
var nest = new NestReader(new MemoryStream(bytes, writable: false)) { ReusableDrawings = reusable }.Read();
|
||||
nest.UpdateDrawingQuantities();
|
||||
return nest;
|
||||
}
|
||||
catch when (live != null)
|
||||
{
|
||||
// Reading adds parts to reused drawings; give the live nest its counts back.
|
||||
live.UpdateDrawingQuantities();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Fingerprints include the name, and a nest's drawings have distinct names, so each
|
||||
// fingerprint names one drawing.
|
||||
private static Dictionary<string, Drawing> ReusableDrawings(Nest live)
|
||||
{
|
||||
var fingerprints = NestWriter.GetDrawingFingerprints(live);
|
||||
var result = new Dictionary<string, Drawing>(StringComparer.Ordinal);
|
||||
var i = 0;
|
||||
// Same order as the fingerprints: both follow the collection's enumeration.
|
||||
foreach (var drawing in live.Drawings)
|
||||
result.TryAdd(fingerprints[i++], drawing);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
-10
@@ -37,6 +37,12 @@ namespace OpenNest.IO
|
||||
/// </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>();
|
||||
@@ -344,7 +350,8 @@ namespace OpenNest.IO
|
||||
// sentinel to new defaults during an edit, so it remains omitted; configured
|
||||
// nondefault clamp sheets are still written.
|
||||
if (
|
||||
plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0
|
||||
!KeepEmptyPlates
|
||||
&& plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0
|
||||
&& plate.CutOffs.Count == 0
|
||||
&& (
|
||||
(plate.ClampZones.Count == 0 && plate.RemovedClamps.Count == 0)
|
||||
@@ -570,17 +577,10 @@ namespace OpenNest.IO
|
||||
{
|
||||
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||
{
|
||||
var drawing = kvp.Value;
|
||||
if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0)
|
||||
var json = GetEntitiesJson(kvp.Value);
|
||||
if (json == null)
|
||||
continue;
|
||||
|
||||
var dto = EntitySerializer.ToDto(
|
||||
drawing.SourceEntities,
|
||||
drawing.SuppressedEntityIds,
|
||||
drawing.EntityRoles
|
||||
);
|
||||
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||
|
||||
var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}");
|
||||
using var stream = entry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
@@ -588,6 +588,62 @@ namespace OpenNest.IO
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user