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;
}
}
}