From 082a50bbcda73e6b04acef16c80c6a24b34a9364 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 31 Oct 2026 22:13:26 -0400 Subject: [PATCH] refactor: remove duplicated client, geometry and desktop code Share the nest server client setup and page validation, entity endpoints, post marker checks and the repeated desktop list, cut-time and busy-warning code. Persistence tests use NestArchiveJson. No behavior change intended. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Core/CNC/HeadHeightLock.cs | 31 ++------ OpenNest.Core/CNC/HeadRaise.cs | 29 ++------ OpenNest.Core/CNC/PostMarkers.cs | 37 ++++++++++ OpenNest.Core/CNC/Program.cs | 30 +------- OpenNest.Core/Geometry/EntityEndpoints.cs | 26 +++++++ OpenNest.Core/Geometry/GeometrySimplifier.cs | 43 ++++------- OpenNest.Core/Geometry/Shape.cs | 26 +------ OpenNest.Core/Geometry/SlideContact.cs | 28 ++------ OpenNest.Data/NestServerClient.cs | 24 +++++++ OpenNest.Data/NestServerHttp.cs | 20 ++++++ OpenNest.Data/RemoteDrawingLibrary.cs | 33 ++------- OpenNest.Data/RemoteDrawingSetStore.cs | 46 +++--------- OpenNest.Data/RemoteNestRepository.cs | 32 ++------- OpenNest.Data/RemoteSharedSettings.cs | 20 +----- .../ClampEdgePersistenceTests.cs | 9 +-- .../ClampRailPersistenceTests.cs | 8 --- OpenNest.IO.Tests/NestArchiveJson.cs | 24 +++++++ .../NestMachinePersistenceTests.cs | 38 +--------- .../SupportGridPersistenceTests.cs | 38 +--------- OpenNest/Controls/FilterPanel.cs | 72 +++++-------------- OpenNest/Forms/EditNestForm.cs | 36 +++------- OpenNest/Forms/MainForm.Guards.cs | 10 +++ OpenNest/Forms/MainForm.Nest.cs | 23 +----- OpenNest/Forms/MainForm.Plate.cs | 6 +- OpenNest/Forms/NestStorage.cs | 42 ++++------- 25 files changed, 249 insertions(+), 482 deletions(-) create mode 100644 OpenNest.Core/CNC/PostMarkers.cs create mode 100644 OpenNest.Core/Geometry/EntityEndpoints.cs create mode 100644 OpenNest.Data/NestServerClient.cs diff --git a/OpenNest.Core/CNC/HeadHeightLock.cs b/OpenNest.Core/CNC/HeadHeightLock.cs index fc739dd3..58973dc9 100644 --- a/OpenNest.Core/CNC/HeadHeightLock.cs +++ b/OpenNest.Core/CNC/HeadHeightLock.cs @@ -73,31 +73,10 @@ namespace OpenNest.CNC /// Refuses a nest a post cannot write, before any output is created: a cut-off with a locked /// overcut needs the post's lock and unlock codes, and its markers must be well formed. /// - public static void EnsurePostable(Nest nest, bool hasCodes, string postName) - { - ArgumentNullException.ThrowIfNull(nest); - foreach (var plate in nest.Plates) - { - foreach (var part in plate.Parts) - { - if (part?.Program == null || !IsMarked(part.Program)) - continue; - var name = DisplayPerimeterTail.PartName(part); - if (!hasCodes) - throw new InvalidOperationException( - $"{name} overcuts past the sheet edge with the head height locked, but the {postName} post " - + "has no head-height lock and unlock codes. Enter them in the post's configuration, " - + "or delete the cut-off and add it again with the head-height lock off."); - try - { - LockedMask(part.Program.Codes); - } - catch (ArgumentException ex) - { - throw new InvalidOperationException($"{name} cannot be posted: {ex.Message}", ex); - } - } - } - } + public static void EnsurePostable(Nest nest, bool hasCodes, string postName) => + PostMarkers.EnsurePostable(nest, hasCodes, IsMarked, program => LockedMask(program.Codes), + name => $"{name} overcuts past the sheet edge with the head height locked, but the {postName} post " + + "has no head-height lock and unlock codes. Enter them in the post's configuration, " + + "or delete the cut-off and add it again with the head-height lock off."); } } diff --git a/OpenNest.Core/CNC/HeadRaise.cs b/OpenNest.Core/CNC/HeadRaise.cs index e88b4b0c..6fa60361 100644 --- a/OpenNest.Core/CNC/HeadRaise.cs +++ b/OpenNest.Core/CNC/HeadRaise.cs @@ -75,30 +75,9 @@ namespace OpenNest.CNC /// Refuses a nest a post cannot write, before any output is created: a marked rapid needs /// the post's head-raise code, and its markers must be well formed. /// - public static void EnsurePostable(Nest nest, bool hasCode, string postName) - { - ArgumentNullException.ThrowIfNull(nest); - foreach (var plate in nest.Plates) - { - foreach (var part in plate.Parts) - { - if (part?.Program == null || !IsMarked(part.Program)) - continue; - var name = DisplayPerimeterTail.PartName(part); - if (!hasCode) - throw new InvalidOperationException( - $"{name} has rapids that must raise the head over parts already cut, but the {postName} " - + "post has no head-raise code. Enter it in the post's configuration before posting."); - try - { - RaisedMask(part.Program.Codes); - } - catch (ArgumentException ex) - { - throw new InvalidOperationException($"{name} cannot be posted: {ex.Message}", ex); - } - } - } - } + public static void EnsurePostable(Nest nest, bool hasCode, string postName) => + PostMarkers.EnsurePostable(nest, hasCode, IsMarked, program => RaisedMask(program.Codes), + name => $"{name} has rapids that must raise the head over parts already cut, but the {postName} " + + "post has no head-raise code. Enter it in the post's configuration before posting."); } } diff --git a/OpenNest.Core/CNC/PostMarkers.cs b/OpenNest.Core/CNC/PostMarkers.cs new file mode 100644 index 00000000..4efb88ae --- /dev/null +++ b/OpenNest.Core/CNC/PostMarkers.cs @@ -0,0 +1,37 @@ +using System; + +namespace OpenNest.CNC +{ + internal static class PostMarkers + { + /// + /// Refuses a nest whose marked programs a post cannot write: every marked part needs the + /// post's , and rejects a malformed + /// program with an . + /// + public static void EnsurePostable(Nest nest, bool hasCode, Func isMarked, + Action validate, Func missingCode) + { + ArgumentNullException.ThrowIfNull(nest); + foreach (var plate in nest.Plates) + { + foreach (var part in plate.Parts) + { + if (part?.Program == null || !isMarked(part.Program)) + continue; + var name = DisplayPerimeterTail.PartName(part); + if (!hasCode) + throw new InvalidOperationException(missingCode(name)); + try + { + validate(part.Program); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException($"{name} cannot be posted: {ex.Message}", ex); + } + } + } + } + } +} diff --git a/OpenNest.Core/CNC/Program.cs b/OpenNest.Core/CNC/Program.cs index 7b02eac4..4d36d64e 100644 --- a/OpenNest.Core/CNC/Program.cs +++ b/OpenNest.Core/CNC/Program.cs @@ -158,35 +158,7 @@ namespace OpenNest.CNC SetModeInc(); } - public virtual void Offset(Vector voffset) - { - var mode = Mode; - - SetModeAbs(); - - for (int i = 0; i < Codes.Count; ++i) - { - var code = Codes[i]; - - if (code is SubProgramCall subpgm) - { - subpgm.Offset = new Geometry.Vector( - subpgm.Offset.X + voffset.X, - subpgm.Offset.Y + voffset.Y - ); - } - - if (code is Motion == false) - continue; - - var code2 = (Motion)code; - - code2.Offset(voffset); - } - - if (mode == Mode.Incremental) - SetModeInc(); - } + public virtual void Offset(Vector voffset) => Offset(voffset.X, voffset.Y); public void LineTo(double x, double y) { diff --git a/OpenNest.Core/Geometry/EntityEndpoints.cs b/OpenNest.Core/Geometry/EntityEndpoints.cs new file mode 100644 index 00000000..368fb6ea --- /dev/null +++ b/OpenNest.Core/Geometry/EntityEndpoints.cs @@ -0,0 +1,26 @@ +namespace OpenNest.Geometry +{ + internal static class EntityEndpoints + { + /// The start and end of a line or arc; false for any other entity. + public static bool TryGet(Entity entity, out Vector start, out Vector end) + { + switch (entity) + { + case Line line: + start = line.StartPoint; + end = line.EndPoint; + return true; + + case Arc arc: + start = arc.StartPoint(); + end = arc.EndPoint(); + return true; + + default: + start = end = default; + return false; + } + } + } +} diff --git a/OpenNest.Core/Geometry/GeometrySimplifier.cs b/OpenNest.Core/Geometry/GeometrySimplifier.cs index 18a7ce85..2e8b8fd4 100644 --- a/OpenNest.Core/Geometry/GeometrySimplifier.cs +++ b/OpenNest.Core/Geometry/GeometrySimplifier.cs @@ -555,13 +555,7 @@ public class GeometrySimplifier if (start > 0) { var prev = entities[start - 1]; - var prevEnd = prev switch - { - Line l => l.EndPoint, - Arc a => a.EndPoint(), - _ => Vector.Invalid, - }; - if (prevEnd.IsValid() && prevEnd.DistanceTo(points[0]) < 1e-6) + if (EntityEndpoints.TryGet(prev, out _, out var prevEnd) && prevEnd.DistanceTo(points[0]) < 1e-6) { if (prev is Arc) return new TangentEstimate(GetExitDirection(prev), true); @@ -596,13 +590,7 @@ public class GeometrySimplifier if (k + 1 < entities.Count) { var next = entities[k + 1]; - var nextStart = next switch - { - Line l => l.StartPoint, - Arc a => a.StartPoint(), - _ => Vector.Invalid, - }; - if (nextStart.IsValid() && nextStart.DistanceTo(points[^1]) < 1e-6) + if (EntityEndpoints.TryGet(next, out var nextStart, out _) && nextStart.DistanceTo(points[^1]) < 1e-6) { if (next is Arc nextArc) return new TangentEstimate(GetEntryDirection(nextArc), true); @@ -654,16 +642,20 @@ public class GeometrySimplifier private static Vector GetEntryDirection(Entity entity) => entity switch { - Line line => new Vector( - line.EndPoint.X - line.StartPoint.X, - line.EndPoint.Y - line.StartPoint.Y - ), - Arc arc => arc.IsReversed - ? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle)) - : new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)), + Line line => LineDirection(line), + Arc arc => ArcTangent(arc, arc.StartAngle), _ => Vector.Invalid, }; + private static Vector LineDirection(Line line) => + new(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y); + + /// The direction of travel along an arc at . + private static Vector ArcTangent(Arc arc, double angle) => + arc.IsReversed + ? new Vector(System.Math.Sin(angle), -System.Math.Cos(angle)) + : new Vector(-System.Math.Sin(angle), System.Math.Cos(angle)); + /// /// Computes the tangent direction at the last point of a fitted arc, /// used to chain tangent continuity to the next arc. @@ -806,13 +798,8 @@ public class GeometrySimplifier private static Vector GetExitDirection(Entity entity) => entity switch { - Line line => new Vector( - line.EndPoint.X - line.StartPoint.X, - line.EndPoint.Y - line.StartPoint.Y - ), - Arc arc => arc.IsReversed - ? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle)) - : new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)), + Line line => LineDirection(line), + Arc arc => ArcTangent(arc, arc.EndAngle), _ => Vector.Invalid, }; diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs index 0791efee..52201344 100644 --- a/OpenNest.Core/Geometry/Shape.cs +++ b/OpenNest.Core/Geometry/Shape.cs @@ -585,8 +585,8 @@ namespace OpenNest.Geometry } if ( - !TryGetEnds(last.Offset, out _, out var gapStart) - || !TryGetEnds(next.Offset, out var gapEnd, out _) + !EntityEndpoints.TryGet(last.Offset, out _, out var gapStart) + || !EntityEndpoints.TryGet(next.Offset, out var gapEnd, out _) ) return; @@ -625,7 +625,7 @@ namespace OpenNest.Geometry corner = default; if ( - !TryGetEnds(last, out _, out corner) + !EntityEndpoints.TryGet(last, out _, out corner) || !TryGetTangents(last, out _, out var d1) || !TryGetTangents(next, out var d2, out _) ) @@ -637,26 +637,6 @@ namespace OpenNest.Geometry || (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon); } - private static bool TryGetEnds(Entity entity, out Vector start, out Vector end) - { - switch (entity) - { - case Line line: - start = line.StartPoint; - end = line.EndPoint; - return true; - - case Arc arc: - start = arc.StartPoint(); - end = arc.EndPoint(); - return true; - - default: - start = end = default; - return false; - } - } - /// /// Direction of travel at the start and end of a line or arc. /// diff --git a/OpenNest.Core/Geometry/SlideContact.cs b/OpenNest.Core/Geometry/SlideContact.cs index 8fb9cbaa..ccda3b41 100644 --- a/OpenNest.Core/Geometry/SlideContact.cs +++ b/OpenNest.Core/Geometry/SlideContact.cs @@ -88,11 +88,11 @@ namespace OpenNest.Geometry } else { - if (!TryEndpoints(entities[i], out var start, out _)) + if (!EntityEndpoints.TryGet(entities[i], out var start, out _)) return Unresolved(entities); var closed = false; - while (i < count && TryEndpoints(entities[i], out _, out var end)) + while (i < count && EntityEndpoints.TryGet(entities[i], out _, out var end)) { // A lone closed arc is a loop; a lone line cannot be, even when it // has zero length and so ends where it starts. @@ -105,7 +105,7 @@ namespace OpenNest.Geometry if ( i + 1 >= count - || !TryEndpoints(entities[i + 1], out var nextStart, out _) + || !EntityEndpoints.TryGet(entities[i + 1], out var nextStart, out _) || !Near(nextStart, end) ) break; @@ -263,11 +263,11 @@ namespace OpenNest.Geometry } private bool StartsNear(int index, Vector point) => - TryEndpoints(entities[index], out var start, out _) + EntityEndpoints.TryGet(entities[index], out var start, out _) && start.DistanceTo(point) <= JointTolerance; private bool EndsNear(int index, Vector point) => - TryEndpoints(entities[index], out _, out var end) + EntityEndpoints.TryGet(entities[index], out _, out var end) && end.DistanceTo(point) <= JointTolerance; private bool InRun(int index, int first, int last) @@ -360,24 +360,6 @@ namespace OpenNest.Geometry } } - private static bool TryEndpoints(Entity entity, out Vector start, out Vector end) - { - switch (entity) - { - case Line line: - start = line.pt1; - end = line.pt2; - return true; - case Arc arc: - start = arc.StartPoint(); - end = arc.EndPoint(); - return true; - default: - start = end = new Vector(); - return false; - } - } - private static bool Near(Vector a, Vector b) => a.DistanceTo(b) <= ChainTolerance; private static double SignedArea(List entities, int first, int last) diff --git a/OpenNest.Data/NestServerClient.cs b/OpenNest.Data/NestServerClient.cs new file mode 100644 index 00000000..bd70c471 --- /dev/null +++ b/OpenNest.Data/NestServerClient.cs @@ -0,0 +1,24 @@ +namespace OpenNest.Data; + +/// The connection every nest server client holds: its HTTP client and the server's base address. +public abstract class NestServerClient : IDisposable +{ + private protected readonly HttpClient _httpClient; + private readonly bool _ownsClient; + private readonly Uri _baseUri; + + private protected NestServerClient(HttpClient httpClient, string baseUrl, bool ownsClient) + { + _httpClient = httpClient; + _ownsClient = ownsClient; + _baseUri = ServerUrl.ParseBase(baseUrl, nameof(baseUrl)); + } + + private protected Uri Url(string relative) => new(_baseUri, relative); + + public void Dispose() + { + if (_ownsClient) + _httpClient.Dispose(); + } +} diff --git a/OpenNest.Data/NestServerHttp.cs b/OpenNest.Data/NestServerHttp.cs index c29ffb63..3dddda8a 100644 --- a/OpenNest.Data/NestServerHttp.cs +++ b/OpenNest.Data/NestServerHttp.cs @@ -21,6 +21,26 @@ internal static class NestServerHttp public static void AddIfMatch(HttpRequestMessage request, int expectedRevision) => request.Headers.IfMatch.Add(new EntityTagHeaderValue($"\"{expectedRevision.ToString(CultureInfo.InvariantCulture)}\"")); + public static void ValidatePaging(int offset, int limit, int maxLimit) + { + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); + if (limit < 1 || limit > maxLimit) throw new ArgumentOutOfRangeException(nameof(limit)); + } + + public static string Paging(int offset, int limit) => + $"offset={offset.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"; + + /// Whether a page is the one asked for and holds no more than its window and total allow. + public static bool IsBoundedPage(int count, int total, int pageOffset, int pageLimit, int offset, int limit) => + count <= limit && pageOffset == offset && pageLimit == limit && total >= 0 + && count <= System.Math.Max(0L, (long)total - offset); + + /// Whether a history page holds only revisions of , newest first. + public static bool IsRevisionHistory(IReadOnlyList items, Guid id, Func idOf, Func revisionOf) + where T : class => + !items.Any(item => item is null || revisionOf(item) < 1 || idOf(item) != id) + && !items.Zip(items.Skip(1)).Any(pair => revisionOf(pair.First) <= revisionOf(pair.Second)); + /// Throws an naming the action, status and the start of the body. public static async Task EnsureSuccessAsync(HttpResponseMessage response, string action, CancellationToken cancellationToken) { diff --git a/OpenNest.Data/RemoteDrawingLibrary.cs b/OpenNest.Data/RemoteDrawingLibrary.cs index 926bee27..830605e4 100644 --- a/OpenNest.Data/RemoteDrawingLibrary.cs +++ b/OpenNest.Data/RemoteDrawingLibrary.cs @@ -16,27 +16,19 @@ namespace OpenNest.Data; /// the library raises ; this client never falls back /// to another store. Other failures surface as . /// -public sealed class RemoteDrawingLibrary : IDrawingLibraryStore, IDisposable +public sealed class RemoteDrawingLibrary : NestServerClient, IDrawingLibraryStore { - private readonly HttpClient _httpClient; - private readonly bool _ownsClient; - private readonly Uri _baseUri; - public RemoteDrawingLibrary(string baseUrl) - : this(new HttpClient(), baseUrl) + : base(new HttpClient(), baseUrl, ownsClient: true) { - _ownsClient = true; } /// For tests and hosts that pool HttpClient instances. public RemoteDrawingLibrary(HttpClient httpClient, string baseUrl) + : base(httpClient, baseUrl, ownsClient: false) { - _httpClient = httpClient; - _baseUri = ServerUrl.ParseBase(baseUrl, nameof(baseUrl)); } - private Uri Url(string relative) => new(_baseUri, relative); - /// /// Stores revision 1 under record.Id, which the caller allocates first so an /// ambiguous failure can be reconciled by id. An existing id raises @@ -106,11 +98,9 @@ public sealed class RemoteDrawingLibrary : IDrawingLibraryStore, IDisposable int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) { ValidateId(id); - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); - if (limit < 1 || limit > DrawingLibraryRules.MaxRevisionLimit) throw new ArgumentOutOfRangeException(nameof(limit)); + NestServerHttp.ValidatePaging(offset, limit, DrawingLibraryRules.MaxRevisionLimit); using var response = await _httpClient.GetAsync( - Url($"api/drawings/{id}/revisions?offset={offset.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"), - cancellationToken); + Url($"api/drawings/{id}/revisions?{NestServerHttp.Paging(offset, limit)}"), cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) { await ThrowIfServerTooOld(cancellationToken); @@ -119,10 +109,8 @@ public sealed class RemoteDrawingLibrary : IDrawingLibraryStore, IDisposable await NestServerHttp.EnsureSuccessAsync(response, $"read library drawing history {id}", cancellationToken); var page = await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken) ?? throw new IOException("Nest server returned an empty drawing history page."); - if (page.Items is null || page.Items.Count > limit || page.Offset != offset || page.Limit != limit - || page.Total < 0 || page.Items.Count > System.Math.Max(0L, (long)page.Total - offset) - || page.Items.Any(item => item is null || item.Revision < 1 || item.Id != id) - || page.Items.Zip(page.Items.Skip(1)).Any(pair => pair.First.Revision <= pair.Second.Revision)) + if (page.Items is null || !NestServerHttp.IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) + || !NestServerHttp.IsRevisionHistory(page.Items, id, item => item.Id, item => item.Revision)) throw new IOException("Nest server returned an invalid or unbounded drawing history page."); return page; } @@ -293,13 +281,6 @@ public sealed class RemoteDrawingLibrary : IDrawingLibraryStore, IDisposable var record = await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken); return record ?? throw new IOException("Nest server returned an empty library drawing record."); } - - - public void Dispose() - { - if (_ownsClient) - _httpClient.Dispose(); - } } /// The nest server predates the drawing library or another feature (it answered HTTP 404). diff --git a/OpenNest.Data/RemoteDrawingSetStore.cs b/OpenNest.Data/RemoteDrawingSetStore.cs index d97d287b..27cab020 100644 --- a/OpenNest.Data/RemoteDrawingSetStore.cs +++ b/OpenNest.Data/RemoteDrawingSetStore.cs @@ -15,7 +15,7 @@ namespace OpenNest.Data; /// before sending. A server without sets raises /// ; other failures surface as . /// -public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable +public sealed class RemoteDrawingSetStore : NestServerClient, IDrawingSetStore { private const string Feature = "drawing sets"; @@ -23,25 +23,17 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable private const string DrawingReleaseFeature = "releasing library drawings on their own"; - private readonly HttpClient _httpClient; - private readonly bool _ownsClient; - private readonly Uri _baseUri; - public RemoteDrawingSetStore(string baseUrl) - : this(new HttpClient(), baseUrl) + : base(new HttpClient(), baseUrl, ownsClient: true) { - _ownsClient = true; } /// For tests and hosts that pool HttpClient instances. public RemoteDrawingSetStore(HttpClient httpClient, string baseUrl) + : base(httpClient, baseUrl, ownsClient: false) { - _httpClient = httpClient; - _baseUri = ServerUrl.ParseBase(baseUrl, nameof(baseUrl)); } - private Uri Url(string relative) => new(_baseUri, relative); - public async Task QueryAsync(DrawingSetQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -206,8 +198,8 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable if (setId == Guid.Empty) throw new ArgumentException("A drawing set id is required.", nameof(setId)); if (nestId == Guid.Empty) throw new ArgumentException("A nest id is required.", nameof(nestId)); if (drawingId == Guid.Empty) throw new ArgumentException("A library drawing id is required.", nameof(drawingId)); - ValidatePaging(offset, limit); - var query = Paging(offset, limit); + NestServerHttp.ValidatePaging(offset, limit, DrawingLibraryRules.MaxRevisionLimit); + var query = NestServerHttp.Paging(offset, limit); if (setId is not null) query += $"&setId={setId}"; if (nestId is not null) query += $"&nestId={nestId}"; if (drawingId is not null) query += $"&drawingId={drawingId}"; @@ -220,7 +212,7 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable await NestServerHttp.EnsureSuccessAsync(response, "read releases", cancellationToken); var page = await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken) ?? throw new IOException("Nest server returned an empty release page."); - if (page.Items is null || !IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) + if (page.Items is null || !NestServerHttp.IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) || page.Items.Any(item => item is null || item.Sets is null || item.Drawings is null || item.Nests is null || (setId is not null && item.Sets.All(s => s?.SetId != setId)) || (nestId is not null && item.Nests.All(n => n?.NestId != nestId)) @@ -327,8 +319,8 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable private async Task GetPageAsync(string relative, int offset, int limit, string action, CancellationToken cancellationToken) { - ValidatePaging(offset, limit); - using var response = await _httpClient.GetAsync(Url($"{relative}?{Paging(offset, limit)}"), cancellationToken); + NestServerHttp.ValidatePaging(offset, limit, DrawingLibraryRules.MaxRevisionLimit); + using var response = await _httpClient.GetAsync(Url($"{relative}?{NestServerHttp.Paging(offset, limit)}"), cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) { await ThrowIfServerTooOld(cancellationToken); @@ -337,7 +329,7 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable await NestServerHttp.EnsureSuccessAsync(response, action, cancellationToken); var page = await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken) ?? throw new IOException("Nest server returned an empty drawing set page."); - if (page.Items is null || !IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) + if (page.Items is null || !NestServerHttp.IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) || page.Items.Any(item => item is null)) throw new IOException("Nest server returned an invalid or unbounded drawing set page."); return page; @@ -359,10 +351,6 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable throw new ServerTooOldException(Feature); } - private static bool IsBoundedPage(int count, int total, int pageOffset, int pageLimit, int offset, int limit) => - count <= limit && pageOffset == offset && pageLimit == limit && total >= 0 - && count <= System.Math.Max(0L, (long)total - offset); - private static bool HasJoinedMembers(DrawingSetRecord record) => record.Members is not null && record.Members.All(m => m is not null && m.Drawing is not null && m.Drawing.Id == m.DrawingId); @@ -432,15 +420,6 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable if (revision < 1) throw new ArgumentOutOfRangeException(nameof(revision)); } - private static void ValidatePaging(int offset, int limit) - { - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); - if (limit < 1 || limit > DrawingLibraryRules.MaxRevisionLimit) throw new ArgumentOutOfRangeException(nameof(limit)); - } - - private static string Paging(int offset, int limit) => - $"offset={offset.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"; - private static string BuildQueryString(DrawingSetQuery query) { var text = new StringBuilder() @@ -469,13 +448,6 @@ public sealed class RemoteDrawingSetStore : IDrawingSetStore, IDisposable throw new IOException("Nest server returned an empty set release record."); return release; } - - - public void Dispose() - { - if (_ownsClient) - _httpClient.Dispose(); - } } /// A guarded set write found a newer or different current state; nothing was changed. diff --git a/OpenNest.Data/RemoteNestRepository.cs b/OpenNest.Data/RemoteNestRepository.cs index 3d302fc9..d3772d3d 100644 --- a/OpenNest.Data/RemoteNestRepository.cs +++ b/OpenNest.Data/RemoteNestRepository.cs @@ -14,28 +14,19 @@ namespace OpenNest.Data; /// a "metadata" JSON part and a "file" part holding the .nest archive. /// Connection and HTTP failures surface as HttpRequestException/IOException to the caller. /// -public sealed class RemoteNestRepository : INestRepository, IDisposable +public sealed class RemoteNestRepository : NestServerClient, INestRepository { - private readonly HttpClient _httpClient; - private readonly bool _ownsClient; - private readonly Uri _baseUri; - public RemoteNestRepository(string baseUrl) - : this(new HttpClient(), baseUrl) + : base(new HttpClient(), baseUrl, ownsClient: true) { - _ownsClient = true; } /// For tests and hosts that pool HttpClient instances. public RemoteNestRepository(HttpClient httpClient, string baseUrl) + : base(httpClient, baseUrl, ownsClient: false) { - _httpClient = httpClient; - _ownsClient = false; - _baseUri = ServerUrl.ParseBase(baseUrl, nameof(baseUrl)); } - private Uri Url(string relative) => new(_baseUri, relative); - public async Task> ListAsync(CancellationToken cancellationToken = default) { using var response = await _httpClient.GetAsync(Url("api/nests"), cancellationToken); @@ -192,19 +183,16 @@ public sealed class RemoteNestRepository : INestRepository, IDisposable CancellationToken cancellationToken) { ValidateId(id); - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); - if (limit < 1 || limit > NestRevisionPage.MaxLimit) throw new ArgumentOutOfRangeException(nameof(limit)); + NestServerHttp.ValidatePaging(offset, limit, NestRevisionPage.MaxLimit); using var response = await _httpClient.GetAsync( - Url($"api/nests/{(deleted ? "deleted/" : "")}{id}/revisions?offset={offset.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"), cancellationToken); + Url($"api/nests/{(deleted ? "deleted/" : "")}{id}/revisions?{NestServerHttp.Paging(offset, limit)}"), cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) throw new IOException("Nest history is unavailable or this nest no longer exists. Upgrade the nest server if it does not support version history, then reload."); await NestServerHttp.EnsureSuccessAsync(response, $"read nest history {id}", cancellationToken); var page = await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken) ?? throw new IOException("Nest server returned an empty history page."); - if (page.Items is null || page.Items.Count > limit || page.Offset != offset || page.Limit != limit - || page.Total < 0 || page.Items.Count > System.Math.Max(0L, (long)page.Total - offset) - || page.Items.Any(item => item is null || item.Revision < 1 || item.Id != id) - || page.Items.Zip(page.Items.Skip(1)).Any(pair => pair.First.Revision <= pair.Second.Revision)) + if (page.Items is null || !NestServerHttp.IsBoundedPage(page.Items.Count, page.Total, page.Offset, page.Limit, offset, limit) + || !NestServerHttp.IsRevisionHistory(page.Items, id, item => item.Id, item => item.Revision)) throw new IOException("Nest server returned an invalid or unbounded history page."); return page; } @@ -308,12 +296,6 @@ public sealed class RemoteNestRepository : INestRepository, IDisposable if (revisionNumber < 1) throw new ArgumentOutOfRangeException(nameof(revisionNumber)); } - public void Dispose() - { - if (_ownsClient) - _httpClient.Dispose(); - } - private static MultipartFormDataContent BuildMultipart(byte[] nestFile, NestRecord record) { var content = new MultipartFormDataContent(); diff --git a/OpenNest.Data/RemoteSharedSettings.cs b/OpenNest.Data/RemoteSharedSettings.cs index 177d0ba2..b97698a9 100644 --- a/OpenNest.Data/RemoteSharedSettings.cs +++ b/OpenNest.Data/RemoteSharedSettings.cs @@ -8,30 +8,22 @@ namespace OpenNest.Data; /// A server without them raises ; a stale write raises /// ; other failures surface as . /// -public sealed class RemoteSharedSettings : IDisposable +public sealed class RemoteSharedSettings : NestServerClient { private const string Feature = "shared machine settings"; - private readonly HttpClient _httpClient; - private readonly bool _ownsClient; - private readonly Uri _baseUri; - /// A client that gives up after , so an unreachable server falls back quickly. public RemoteSharedSettings(string baseUrl, TimeSpan timeout) - : this(new HttpClient { Timeout = timeout }, baseUrl) + : base(new HttpClient { Timeout = timeout }, baseUrl, ownsClient: true) { - _ownsClient = true; } /// For tests and hosts that pool HttpClient instances. public RemoteSharedSettings(HttpClient httpClient, string baseUrl) + : base(httpClient, baseUrl, ownsClient: false) { - _httpClient = httpClient; - _baseUri = ServerUrl.ParseBase(baseUrl, nameof(baseUrl)); } - private Uri Url(string relative) => new(_baseUri, relative); - private static string Path(string kind, string key) => $"api/settings/{Uri.EscapeDataString(kind)}/{Uri.EscapeDataString(key)}"; @@ -122,12 +114,6 @@ public sealed class RemoteSharedSettings : IDisposable private static async Task ReadRecord(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadFromJsonAsync(NestServerHttp.JsonOptions, cancellationToken) ?? throw new IOException("Nest server returned empty settings."); - - public void Dispose() - { - if (_ownsClient) - _httpClient.Dispose(); - } } /// A shared settings write found a newer or different stored document; nothing was changed. diff --git a/OpenNest.IO.Tests/ClampEdgePersistenceTests.cs b/OpenNest.IO.Tests/ClampEdgePersistenceTests.cs index 90a88c9e..a151967c 100644 --- a/OpenNest.IO.Tests/ClampEdgePersistenceTests.cs +++ b/OpenNest.IO.Tests/ClampEdgePersistenceTests.cs @@ -2,6 +2,7 @@ using System.IO.Compression; using System.Text; using System.Text.Json.Nodes; using OpenNest.Geometry; +using static OpenNest.IO.Tests.NestArchiveJson; namespace OpenNest.IO.Tests; @@ -29,14 +30,6 @@ public class ClampEdgePersistenceTests return nest; } - private static MemoryStream Write(Nest nest) - { - var stream = new MemoryStream(); - Assert.True(new NestWriter(nest).Write(stream)); - stream.Position = 0; - return stream; - } - private static JsonNode RailJson(MemoryStream zip) { zip.Position = 0; diff --git a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs index 915dbe1d..3eda5458 100644 --- a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs +++ b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs @@ -32,14 +32,6 @@ public class ClampRailPersistenceTests return nest; } - private static MemoryStream Write(Nest nest) - { - var stream = new MemoryStream(); - Assert.True(new NestWriter(nest).Write(stream)); - stream.Position = 0; - return stream; - } - private static Nest RoundTrip(Nest nest) => new NestReader(Write(nest)).Read(); [Fact] diff --git a/OpenNest.IO.Tests/NestArchiveJson.cs b/OpenNest.IO.Tests/NestArchiveJson.cs index 3fcd5b97..2bec0778 100644 --- a/OpenNest.IO.Tests/NestArchiveJson.cs +++ b/OpenNest.IO.Tests/NestArchiveJson.cs @@ -16,6 +16,30 @@ internal static class NestArchiveJson return (reader.Read(), reader.Warnings); } + public static MemoryStream Write(Nest nest) + { + var stream = new MemoryStream(); + Assert.True(new NestWriter(nest).Write(stream)); + stream.Position = 0; + return stream; + } + + /// The archive's nest.json, leaving ready to read again. + public static JsonObject NestJson(MemoryStream zip) + { + var json = NestJson(zip.ToArray()); + zip.Position = 0; + return json; + } + + /// A copy of the archive whose nest.json was changed by . + public static MemoryStream Rewrite(MemoryStream zip, Action edit) + { + var json = NestJson(zip); + edit(json); + return new MemoryStream(ReplaceNestJson(zip.ToArray(), json)); + } + public static JsonObject NestJson(byte[] archive) { using var zip = new ZipArchive(new MemoryStream(archive), ZipArchiveMode.Read); diff --git a/OpenNest.IO.Tests/NestMachinePersistenceTests.cs b/OpenNest.IO.Tests/NestMachinePersistenceTests.cs index 2a6aa050..e412bf72 100644 --- a/OpenNest.IO.Tests/NestMachinePersistenceTests.cs +++ b/OpenNest.IO.Tests/NestMachinePersistenceTests.cs @@ -1,7 +1,6 @@ -using System.IO.Compression; -using System.Text; using System.Text.Json.Nodes; using OpenNest.Geometry; +using static OpenNest.IO.Tests.NestArchiveJson; namespace OpenNest.IO.Tests; @@ -21,41 +20,6 @@ public class NestMachinePersistenceTests return nest; } - private static MemoryStream Write(Nest nest) - { - var stream = new MemoryStream(); - Assert.True(new NestWriter(nest).Write(stream)); - stream.Position = 0; - return stream; - } - - private static JsonNode NestJson(MemoryStream zip) - { - zip.Position = 0; - using var archive = new ZipArchive(zip, ZipArchiveMode.Read, leaveOpen: true); - using var reader = new StreamReader(archive.GetEntry("nest.json")!.Open(), Encoding.UTF8); - var json = JsonNode.Parse(reader.ReadToEnd())!; - zip.Position = 0; - return json; - } - - private static MemoryStream Rewrite(MemoryStream zip, Action edit) - { - var json = NestJson(zip); - edit(json); - var copy = new MemoryStream(); - zip.Position = 0; - zip.CopyTo(copy); - using (var archive = new ZipArchive(copy, ZipArchiveMode.Update, leaveOpen: true)) - { - archive.GetEntry("nest.json")!.Delete(); - using var writer = new StreamWriter(archive.CreateEntry("nest.json").Open(), new UTF8Encoding(false)); - writer.Write(json.ToJsonString()); - } - copy.Position = 0; - return copy; - } - [Fact] public void MachineRoundTrips() { diff --git a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs index 1be81a6c..88cad8dd 100644 --- a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs +++ b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs @@ -1,9 +1,8 @@ -using System.IO.Compression; -using System.Text; using System.Text.Json.Nodes; using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; using OpenNest.Support; +using static OpenNest.IO.Tests.NestArchiveJson; namespace OpenNest.IO.Tests; @@ -25,41 +24,6 @@ public class SupportGridPersistenceTests return nest; } - private static MemoryStream Write(Nest nest) - { - var stream = new MemoryStream(); - Assert.True(new NestWriter(nest).Write(stream)); - stream.Position = 0; - return stream; - } - - private static JsonNode NestJson(MemoryStream zip) - { - zip.Position = 0; - using var archive = new ZipArchive(zip, ZipArchiveMode.Read, leaveOpen: true); - using var reader = new StreamReader(archive.GetEntry("nest.json")!.Open(), Encoding.UTF8); - var json = JsonNode.Parse(reader.ReadToEnd())!; - zip.Position = 0; - return json; - } - - private static MemoryStream Rewrite(MemoryStream zip, Action edit) - { - var json = NestJson(zip); - edit(json); - var copy = new MemoryStream(); - zip.Position = 0; - zip.CopyTo(copy); - using (var archive = new ZipArchive(copy, ZipArchiveMode.Update, leaveOpen: true)) - { - archive.GetEntry("nest.json")!.Delete(); - using var writer = new StreamWriter(archive.CreateEntry("nest.json").Open(), new UTF8Encoding(false)); - writer.Write(json.ToJsonString()); - } - copy.Position = 0; - return copy; - } - [Fact] public void GridRoundTripsAndEveryPlateFollowsIt() { diff --git a/OpenNest/Controls/FilterPanel.cs b/OpenNest/Controls/FilterPanel.cs index 1f962b99..c331f652 100644 --- a/OpenNest/Controls/FilterPanel.cs +++ b/OpenNest/Controls/FilterPanel.cs @@ -559,27 +559,8 @@ namespace OpenNest.Controls } /// Shows one line per breakoff with selected, without raising selection events. - public void LoadBreakoffs(IReadOnlyList labels, int selected) - { - var added = labels.Count > breakoffsList.Items.Count; - loadingBreakoffs = true; - try - { - breakoffsList.BeginUpdate(); - breakoffsList.Items.Clear(); - foreach (var label in labels) - breakoffsList.Items.Add(label); - breakoffsList.SelectedIndex = selected >= 0 && selected < labels.Count ? selected : -1; - breakoffsList.EndUpdate(); - } - finally - { - loadingBreakoffs = false; - } - breakoffsPanel.HeaderText = $"Breakoffs ({labels.Count})"; - if (added) - breakoffsPanel.IsExpanded = true; - } + public void LoadBreakoffs(IReadOnlyList labels, int selected) => + LoadList(breakoffsList, breakoffsPanel, "Breakoffs", ref loadingBreakoffs, labels, selected); public void SetTabbedCutoutPickMode(bool active) { @@ -588,49 +569,34 @@ namespace OpenNest.Controls } /// Shows one line per tabbed cutout with selected, without raising selection events. - public void LoadTabbedCutouts(IReadOnlyList labels, int selected) - { - var added = labels.Count > cutoutsList.Items.Count; - loadingCutouts = true; - try - { - cutoutsList.BeginUpdate(); - cutoutsList.Items.Clear(); - foreach (var label in labels) - cutoutsList.Items.Add(label); - cutoutsList.SelectedIndex = selected >= 0 && selected < labels.Count ? selected : -1; - cutoutsList.EndUpdate(); - } - finally - { - loadingCutouts = false; - } - cutoutsPanel.HeaderText = $"Tabbed Cutouts ({labels.Count})"; - if (added) - cutoutsPanel.IsExpanded = true; - } + public void LoadTabbedCutouts(IReadOnlyList labels, int selected) => + LoadList(cutoutsList, cutoutsPanel, "Tabbed Cutouts", ref loadingCutouts, labels, selected); /// Shows one line per dimension with selected, without raising selection events. - public void LoadDimensions(IReadOnlyList labels, int selected) + public void LoadDimensions(IReadOnlyList labels, int selected) => + LoadList(dimensionsList, dimensionsPanel, "Dimensions", ref loadingDimensions, labels, selected); + + private static void LoadList(ListBox list, CollapsiblePanel panel, string title, ref bool loading, + IReadOnlyList labels, int selected) { - var added = labels.Count > dimensionsList.Items.Count; - loadingDimensions = true; + var added = labels.Count > list.Items.Count; + loading = true; try { - dimensionsList.BeginUpdate(); - dimensionsList.Items.Clear(); + list.BeginUpdate(); + list.Items.Clear(); foreach (var label in labels) - dimensionsList.Items.Add(label); - dimensionsList.SelectedIndex = selected >= 0 && selected < labels.Count ? selected : -1; - dimensionsList.EndUpdate(); + list.Items.Add(label); + list.SelectedIndex = selected >= 0 && selected < labels.Count ? selected : -1; + list.EndUpdate(); } finally { - loadingDimensions = false; + loading = false; } - dimensionsPanel.HeaderText = $"Dimensions ({labels.Count})"; + panel.HeaderText = $"{title} ({labels.Count})"; if (added) - dimensionsPanel.IsExpanded = true; + panel.IsExpanded = true; } } diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index e6e27fab..5554050a 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1110,7 +1110,13 @@ namespace OpenNest.Forms PlateSequencing.Apply(plate, parameters); } - public void CalculateCurrentPlateCutTime() + public void CalculateCurrentPlateCutTime() => + ShowCutTime(() => Timing.GetTimingInfo(PlateView.Plate), cutparams => Timing.CalculateTime(PlateView.Plate, cutparams)); + + public void CalculateNestCutTime() => + ShowCutTime(() => Timing.GetTimingInfo(Nest), cutparams => Timing.CalculateTime(Nest, cutparams)); + + private void ShowCutTime(Func getInfo, Func calculateTime) { var cutParamsForm = new CutParametersForm(); cutParamsForm.Units = Nest.Units; @@ -1118,8 +1124,8 @@ namespace OpenNest.Forms if (cutParamsForm.ShowDialog() == DialogResult.OK) { var cutparams = cutParamsForm.GetCutParameters(); - var info = Timing.GetTimingInfo(PlateView.Plate); - var time = Timing.CalculateTime(PlateView.Plate, cutparams); + var info = getInfo(); + var time = calculateTime(cutparams); var timingForm = new TimingForm(); timingForm.Units = Nest.Units; @@ -1155,30 +1161,6 @@ namespace OpenNest.Forms System.Math.Max(owner.Top, corner.Y - cutSimulatorForm.Height - 48)); } - public void CalculateNestCutTime() - { - var cutParamsForm = new CutParametersForm(); - cutParamsForm.Units = Nest.Units; - - if (cutParamsForm.ShowDialog() == DialogResult.OK) - { - var cutparams = cutParamsForm.GetCutParameters(); - var info = Timing.GetTimingInfo(Nest); - var time = Timing.CalculateTime(Nest, cutparams); - - var timingForm = new TimingForm(); - timingForm.Units = Nest.Units; - timingForm.SetCutDistance(info.CutDistance); - timingForm.SetCutTime(time); - timingForm.SetIntersectionCount(info.IntersectionCount); - timingForm.SetPierceCount(info.PierceCount); - timingForm.SetRapidDistance(info.TravelDistance); - timingForm.SetCutParameters(cutparams); - - timingForm.ShowDialog(); - } - } - private void FirePlateChanged(bool updateListView = true) { if (updateListView) diff --git a/OpenNest/Forms/MainForm.Guards.cs b/OpenNest/Forms/MainForm.Guards.cs index 44cfd984..19438a40 100644 --- a/OpenNest/Forms/MainForm.Guards.cs +++ b/OpenNest/Forms/MainForm.Guards.cs @@ -29,6 +29,16 @@ namespace OpenNest.Forms IsJobRunning || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true); + /// Tells the user to finish first when ; true if it was. + private bool WarnIfPlateWorkBusy(PlateView[] views, string caption) + { + if (!IsPlateWorkBusy(views)) + return false; + MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", + caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + return true; + } + /// /// A running job or a fill on any view of ; a busy current /// action does not count. diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index 1693192c..d97bfa63 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -90,12 +90,8 @@ namespace OpenNest.Forms var nest = editForm.Nest; var views = ViewsOf(nest); - if (IsPlateWorkBusy(views)) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", - "Machine", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (WarnIfPlateWorkBusy(views, "Machine")) return; - } editForm.RecordChange("Machine", () => { @@ -125,17 +121,8 @@ namespace OpenNest.Forms var nest = editForm.Nest; var views = ViewsOf(nest); - if (IsPlateWorkBusy(views)) - { - MessageBox.Show( - this, - "Finish or cancel the current nesting or plate action first.", - "Clamps", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); + if (WarnIfPlateWorkBusy(views, "Clamps")) return; - } var machine = MachineSettings.FindMachine(nest.Machine); Action save = machine == null @@ -160,12 +147,8 @@ namespace OpenNest.Forms var nest = editForm.Nest; var views = ViewsOf(nest); - if (IsPlateWorkBusy(views)) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", - "Support Bed", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (WarnIfPlateWorkBusy(views, "Support Bed")) return; - } var machine = MachineSettings.FindMachine(nest.Machine); Action save = machine == null diff --git a/OpenNest/Forms/MainForm.Plate.cs b/OpenNest/Forms/MainForm.Plate.cs index 6e46e9c3..7bd13992 100644 --- a/OpenNest/Forms/MainForm.Plate.cs +++ b/OpenNest/Forms/MainForm.Plate.cs @@ -86,12 +86,8 @@ namespace OpenNest.Forms var nest = editForm.Nest; var views = ViewsOf(nest); bool IsBusy() => IsPlateWorkBusy(views); - if (IsBusy()) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", - "Automatic Scrap Cutoffs", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (WarnIfPlateWorkBusy(views, "Automatic Scrap Cutoffs")) return; - } try { diff --git a/OpenNest/Forms/NestStorage.cs b/OpenNest/Forms/NestStorage.cs index af602ee6..5ee19b38 100644 --- a/OpenNest/Forms/NestStorage.cs +++ b/OpenNest/Forms/NestStorage.cs @@ -43,48 +43,34 @@ namespace OpenNest.Forms /// URL changes. Throws if the configured URL /// is not a valid absolute http/https address. /// - public static INestRepository CreateRepository() - { - if (cachedRepository == null || cachedServerUrl != settings.ServerUrl) - { - cachedRepository?.Dispose(); - cachedRepository = new RemoteNestRepository(settings.ServerUrl); - cachedServerUrl = settings.ServerUrl; - } - - return cachedRepository; - } + public static INestRepository CreateRepository() => + GetOrCreate(ref cachedRepository, ref cachedServerUrl, url => new RemoteNestRepository(url)); /// /// The shared drawing library client for the current server URL, recreated only when /// the URL changes. Throws for an invalid URL. /// - public static IDrawingLibraryStore CreateDrawingLibrary() - { - if (cachedLibrary == null || cachedLibraryUrl != settings.ServerUrl) - { - cachedLibrary?.Dispose(); - cachedLibrary = new RemoteDrawingLibrary(settings.ServerUrl); - cachedLibraryUrl = settings.ServerUrl; - } - - return cachedLibrary; - } + public static IDrawingLibraryStore CreateDrawingLibrary() => + GetOrCreate(ref cachedLibrary, ref cachedLibraryUrl, url => new RemoteDrawingLibrary(url)); /// /// The shared drawing set client for the current server URL, recreated only when the /// URL changes. Throws for an invalid URL. /// - public static IDrawingSetStore CreateDrawingSetStore() + public static IDrawingSetStore CreateDrawingSetStore() => + GetOrCreate(ref cachedSets, ref cachedSetsUrl, url => new RemoteDrawingSetStore(url)); + + private static T GetOrCreate(ref T cached, ref string cachedUrl, Func create) + where T : NestServerClient { - if (cachedSets == null || cachedSetsUrl != settings.ServerUrl) + if (cached == null || cachedUrl != settings.ServerUrl) { - cachedSets?.Dispose(); - cachedSets = new RemoteDrawingSetStore(settings.ServerUrl); - cachedSetsUrl = settings.ServerUrl; + cached?.Dispose(); + cached = create(settings.ServerUrl); + cachedUrl = settings.ServerUrl; } - return cachedSets; + return cached; } } }