namespace OpenNest.Data; /// /// A library drawing's editable details. Units are not among them: they give the stored /// geometry its meaning, so changing them would rescale the part. /// public sealed record DrawingLibraryDetails( string Name, string Customer, string MaterialName, string MaterialGrade, double Thickness, CutType CutType); /// /// Edits one listed library drawing's details (name, customer, material, grade, thickness, /// cut type), notes and nesting settings in place: the save is a metadata-only revision /// guarded by the revision it was listed with, so the package (and its hash) stays the same /// and nests keep the revision they hold. A stale revision raises /// and changes nothing. /// public sealed class DrawingLibraryEditSession { private readonly IDrawingLibraryStore _library; public DrawingLibraryEditSession(IDrawingLibraryStore library, DrawingLibraryRecord listed, string savedBy) { _library = library ?? throw new ArgumentNullException(nameof(library)); Listed = listed ?? throw new ArgumentNullException(nameof(listed)); if (listed.Id == Guid.Empty || listed.Revision < 1) throw new ArgumentException("Choose a listed library drawing to edit.", nameof(listed)); if (listed.Archived) throw new ArgumentException("An archived library drawing cannot be edited. Restore it first.", nameof(listed)); SavedBy = savedBy ?? ""; } /// The record as it was listed; its revision guards the save. public DrawingLibraryRecord Listed { get; } public string SavedBy { get; } /// The listed record's details, the starting point of an edit. public DrawingLibraryDetails Details => new(Listed.Name, Listed.Customer, Listed.MaterialName, Listed.MaterialGrade, Listed.Thickness, Listed.CutType); /// /// The record to save: the edited details (text trimmed, thickness rounded as stored), /// the listed units, and the new notes and settings. /// public DrawingLibraryRecord Build(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings) { ArgumentNullException.ThrowIfNull(details); return new DrawingLibraryRecord { Id = Listed.Id, Name = (details.Name ?? "").Trim(), Customer = (details.Customer ?? "").Trim(), MaterialName = (details.MaterialName ?? "").Trim(), MaterialGrade = (details.MaterialGrade ?? "").Trim(), Thickness = double.IsFinite(details.Thickness) ? DrawingLibraryRules.NormalizeThickness(details.Thickness) : details.Thickness, Units = Listed.Units, CutType = details.CutType, Notes = notes ?? "", Settings = settings ?? throw new ArgumentNullException(nameof(settings)), SavedBy = SavedBy, }; } /// The first rule the edit breaks, or null when it may be saved. public string? GetError(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings) => DrawingLibraryRules.GetMetadataError(Build(details, notes, settings)); /// True when saving would change nothing. public bool IsUnchanged(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings) => SameDetails(Build(details, notes, settings), Listed) && (notes ?? "") == Listed.Notes && Equals(settings, Listed.Settings); /// /// True when the edit changes the details that identify a library drawing (D4: customer, /// name, material, grade, thickness and cut type, normalized as duplicates are matched). /// public bool ChangesIdentity(DrawingLibraryDetails details) => !DrawingLibraryDuplicates.SameDuplicateKey(BuildDetails(details), Listed); /// /// The other active library drawings the edited details would duplicate (D4), newest /// first. Empty when the edit keeps this drawing's identity, so drawings that already /// shared it are not reported on every save. /// public async Task> FindCollisionsAsync(DrawingLibraryDetails details, CancellationToken cancellationToken = default) { var record = BuildDetails(details); if (!ChangesIdentity(details) || DrawingLibraryRules.GetMetadataError(record) != null) return Array.Empty(); var matches = await DrawingLibraryDuplicates.FindMatchesAsync(_library, record, cancellationToken); return matches.Where(m => m.Id != Listed.Id).ToList(); } // The record with the edited details and the listed notes and settings, for identity checks. private DrawingLibraryRecord BuildDetails(DrawingLibraryDetails details) => Build(details, Listed.Notes, Listed.Settings ?? new DrawingNestingSettings()); /// Saves the edit as the next revision; returns the stored record. public async Task SaveAsync(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings, CancellationToken cancellationToken = default) { var record = Build(details, notes, settings); var error = DrawingLibraryRules.GetMetadataError(record); if (error != null) throw new ArgumentException(error, nameof(details)); var saved = await _library.UpdateMetadataAsync(Listed.Id, record, Listed.Revision, cancellationToken); if (!string.Equals(saved.PackageSha256, Listed.PackageSha256, StringComparison.OrdinalIgnoreCase) || !SameDetails(saved, record) || saved.Units != record.Units || !Equals(saved.Settings, record.Settings) || saved.Notes != record.Notes) throw new IOException("The nest server stored a different library drawing than was edited. Reload the library."); return saved; } // Compares a built (trimmed, rounded) record with another, as stored. private static bool SameDetails(DrawingLibraryRecord built, DrawingLibraryRecord other) => built.Name == other.Name.Trim() && built.Customer == other.Customer.Trim() && built.MaterialName == other.MaterialName.Trim() && built.MaterialGrade == other.MaterialGrade.Trim() && built.Thickness == DrawingLibraryRules.NormalizeThickness(other.Thickness) && built.CutType == other.CutType; }