mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 23:01:40 -05:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
31525ba342
commit
082a50bbcd
25 files changed
+249
-482
No files matched your search
@@ -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.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
internal static class PostMarkers
|
||||
{
|
||||
/// <summary>
|
||||
/// Refuses a nest whose marked programs a post cannot write: every marked part needs the
|
||||
/// post's <paramref name="hasCode"/>, and <paramref name="validate"/> rejects a malformed
|
||||
/// program with an <see cref="ArgumentException"/>.
|
||||
/// </summary>
|
||||
public static void EnsurePostable(Nest nest, bool hasCode, Func<Program, bool> isMarked,
|
||||
Action<Program> validate, Func<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
internal static class EntityEndpoints
|
||||
{
|
||||
/// <summary>The start and end of a line or arc; false for any other entity.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/// <summary>The direction of travel along an arc at <paramref name="angle"/>.</summary>
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// 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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direction of travel at the start and end of a line or arc.
|
||||
/// </summary>
|
||||
|
||||
@@ -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<Entity> entities, int first, int last)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>The connection every nest server client holds: its HTTP client and the server's base address.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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)}";
|
||||
|
||||
/// <summary>Whether a page is the one asked for and holds no more than its window and total allow.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Whether a history page holds only revisions of <paramref name="id"/>, newest first.</summary>
|
||||
public static bool IsRevisionHistory<T>(IReadOnlyList<T> items, Guid id, Func<T, Guid> idOf, Func<T, int> 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));
|
||||
|
||||
/// <summary>Throws an <see cref="IOException"/> naming the action, status and the start of the body.</summary>
|
||||
public static async Task EnsureSuccessAsync(HttpResponseMessage response, string action, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -16,27 +16,19 @@ namespace OpenNest.Data;
|
||||
/// the library raises <see cref="ServerTooOldException"/>; this client never falls back
|
||||
/// to another store. Other failures surface as <see cref="IOException"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>For tests and hosts that pool HttpClient instances.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Stores revision 1 under <c>record.Id</c>, 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<DrawingLibraryPage>(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<DrawingLibraryRecord>(NestServerHttp.JsonOptions, cancellationToken);
|
||||
return record ?? throw new IOException("Nest server returned an empty library drawing record.");
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_ownsClient)
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The nest server predates the drawing library or another feature (it answered HTTP 404).</summary>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace OpenNest.Data;
|
||||
/// <see cref="SetReleaseRules"/> before sending. A server without sets raises
|
||||
/// <see cref="ServerTooOldException"/>; other failures surface as <see cref="IOException"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>For tests and hosts that pool HttpClient instances.</summary>
|
||||
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<DrawingSetPage> 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<SetReleasePage>(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<DrawingSetPage?> 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<DrawingSetPage>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A guarded set write found a newer or different current state; nothing was changed.</summary>
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>For tests and hosts that pool HttpClient instances.</summary>
|
||||
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<IReadOnlyList<NestRecord>> 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<NestRevisionPage>(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();
|
||||
|
||||
@@ -8,30 +8,22 @@ namespace OpenNest.Data;
|
||||
/// A server without them raises <see cref="ServerTooOldException"/>; a stale write raises
|
||||
/// <see cref="SharedSettingSaveConflictException"/>; other failures surface as <see cref="IOException"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>A client that gives up after <paramref name="timeout"/>, so an unreachable server falls back quickly.</summary>
|
||||
public RemoteSharedSettings(string baseUrl, TimeSpan timeout)
|
||||
: this(new HttpClient { Timeout = timeout }, baseUrl)
|
||||
: base(new HttpClient { Timeout = timeout }, baseUrl, ownsClient: true)
|
||||
{
|
||||
_ownsClient = true;
|
||||
}
|
||||
|
||||
/// <summary>For tests and hosts that pool HttpClient instances.</summary>
|
||||
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<SharedSettingRecord> ReadRecord(HttpResponseMessage response, CancellationToken cancellationToken) =>
|
||||
await response.Content.ReadFromJsonAsync<SharedSettingRecord>(NestServerHttp.JsonOptions, cancellationToken)
|
||||
?? throw new IOException("Nest server returned empty settings.");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_ownsClient)
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A shared settings write found a newer or different stored document; nothing was changed.</summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>The archive's nest.json, leaving <paramref name="zip"/> ready to read again.</summary>
|
||||
public static JsonObject NestJson(MemoryStream zip)
|
||||
{
|
||||
var json = NestJson(zip.ToArray());
|
||||
zip.Position = 0;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>A copy of the archive whose nest.json was changed by <paramref name="edit"/>.</summary>
|
||||
public static MemoryStream Rewrite(MemoryStream zip, Action<JsonObject> 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);
|
||||
|
||||
@@ -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<JsonNode> 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()
|
||||
{
|
||||
|
||||
@@ -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<JsonNode> 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()
|
||||
{
|
||||
|
||||
@@ -559,27 +559,8 @@ namespace OpenNest.Controls
|
||||
}
|
||||
|
||||
/// <summary>Shows one line per breakoff with <paramref name="selected"/> selected, without raising selection events.</summary>
|
||||
public void LoadBreakoffs(IReadOnlyList<string> 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<string> labels, int selected) =>
|
||||
LoadList(breakoffsList, breakoffsPanel, "Breakoffs", ref loadingBreakoffs, labels, selected);
|
||||
|
||||
public void SetTabbedCutoutPickMode(bool active)
|
||||
{
|
||||
@@ -588,49 +569,34 @@ namespace OpenNest.Controls
|
||||
}
|
||||
|
||||
/// <summary>Shows one line per tabbed cutout with <paramref name="selected"/> selected, without raising selection events.</summary>
|
||||
public void LoadTabbedCutouts(IReadOnlyList<string> 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<string> labels, int selected) =>
|
||||
LoadList(cutoutsList, cutoutsPanel, "Tabbed Cutouts", ref loadingCutouts, labels, selected);
|
||||
|
||||
/// <summary>Shows one line per dimension with <paramref name="selected"/> selected, without raising selection events.</summary>
|
||||
public void LoadDimensions(IReadOnlyList<string> labels, int selected)
|
||||
public void LoadDimensions(IReadOnlyList<string> 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<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TimingInfo> getInfo, Func<CutParameters, TimeSpan> 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)
|
||||
|
||||
@@ -29,6 +29,16 @@ namespace OpenNest.Forms
|
||||
IsJobRunning
|
||||
|| views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true);
|
||||
|
||||
/// <summary>Tells the user to finish first when <see cref="IsPlateWorkBusy"/>; true if it was.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A running job or a fill on any view of <paramref name="nest"/>; a busy current
|
||||
/// action does not count.
|
||||
|
||||
@@ -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<ClampSettings> 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<SupportGridSettings> save = machine == null
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -43,48 +43,34 @@ namespace OpenNest.Forms
|
||||
/// URL changes. Throws <see cref="ArgumentException"/> if the configured URL
|
||||
/// is not a valid absolute http/https address.
|
||||
/// </summary>
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// The shared drawing library client for the current server URL, recreated only when
|
||||
/// the URL changes. Throws <see cref="ArgumentException"/> for an invalid URL.
|
||||
/// </summary>
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// The shared drawing set client for the current server URL, recreated only when the
|
||||
/// URL changes. Throws <see cref="ArgumentException"/> for an invalid URL.
|
||||
/// </summary>
|
||||
public static IDrawingSetStore CreateDrawingSetStore()
|
||||
public static IDrawingSetStore CreateDrawingSetStore() =>
|
||||
GetOrCreate(ref cachedSets, ref cachedSetsUrl, url => new RemoteDrawingSetStore(url));
|
||||
|
||||
private static T GetOrCreate<T>(ref T cached, ref string cachedUrl, Func<string, T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user