mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 17:01:38 -05:00
feat(nesting): nest higher priority numbers first
Priority worked like a rank (lower first) with a default of 0, and the Edit Drawing form stopped at 0, so moving one part ahead of the rest meant renumbering every other drawing. Higher-first lets a user raise a single part to 1 and leave everything else at the default. Every engine, filler and candidate comparer now serves the highest tier first. Stored values keep their order: - .nest files add `nestPriority` and keep writing the negated value under `priority`, so older readers nest in the same order; files without `nestPriority` are negated on read. Drawing packages strip both, so package hashes are unchanged. - Library settings send both keys the same way for older clients. - Server schema v18 negates stored drawing priorities (an older server refuses the upgraded database). - Job documents move to version 2; version 1 jobs are converted, and an older worker refuses version 2 instead of reversing the order. The Edit Drawing form now accepts negative priorities and says higher numbers nest first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2721375e8b
commit
fbc23d6b64
51 files changed
+364
-79
No files matched your search
@@ -7,5 +7,6 @@ public class NestRequestPart
|
||||
public string DxfPath { get; init; }
|
||||
public int Quantity { get; init; } = 1;
|
||||
public bool AllowRotation { get; init; } = true;
|
||||
/// <summary>Nesting order: higher numbers nest first.</summary>
|
||||
public int Priority { get; init; } = 0;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ namespace OpenNest
|
||||
|
||||
public string Customer { get; set; }
|
||||
|
||||
/// <summary>Nesting order: higher numbers nest first. Zero is the default.</summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
public DwgQty Quantity;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Nesting priority places higher numbers first. Files, the drawing library and server job
|
||||
/// documents written before that stored a lower-first value under <c>priority</c>; they keep
|
||||
/// writing it beside the new value so older readers see the same order. Converting negates,
|
||||
/// which keeps the order and is its own inverse.
|
||||
/// </summary>
|
||||
public static class LegacyPriority
|
||||
{
|
||||
/// <summary>Converts between a higher-first priority and its lower-first legacy value, either way.</summary>
|
||||
public static int Convert(int priority) => priority == int.MinValue ? int.MaxValue : -priority;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenNest;
|
||||
|
||||
namespace OpenNest.Data;
|
||||
@@ -14,8 +15,29 @@ public sealed record DrawingNestingSettings
|
||||
/// <summary>Automatic sequencing cuts this drawing's parts first (for example test squares).</summary>
|
||||
public bool CutFirst { get; init; }
|
||||
|
||||
/// <summary>Nesting order: higher numbers nest first.</summary>
|
||||
[JsonIgnore]
|
||||
public int Priority { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The lower-first legacy value (<see cref="OpenNest.LegacyPriority"/>) older clients read
|
||||
/// and send. Written before <see cref="NestPriority"/>, so a reader that sees both ends on it.
|
||||
/// </summary>
|
||||
[JsonPropertyName("priority")]
|
||||
public int LegacyPriorityValue
|
||||
{
|
||||
get => LegacyPriority.Convert(Priority);
|
||||
init => Priority = LegacyPriority.Convert(value);
|
||||
}
|
||||
|
||||
/// <summary><see cref="Priority"/> on the wire; absent from older clients.</summary>
|
||||
[JsonPropertyName("nestPriority")]
|
||||
public int NestPriority
|
||||
{
|
||||
get => Priority;
|
||||
init => Priority = value;
|
||||
}
|
||||
|
||||
/// <summary>Rotation step in radians; zero means the engine's default.</summary>
|
||||
public double StepAngle { get; init; }
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using OpenNest.Engine.Jobs.Remote;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Version 2 job documents nest higher priority numbers first. Version 1 documents came from
|
||||
/// desktops that nested lower numbers first and are converted when read, so a job queued before
|
||||
/// an upgrade keeps its order.
|
||||
/// </summary>
|
||||
public class NestJobDocumentPriorityTests
|
||||
{
|
||||
[Fact]
|
||||
public void CurrentVersionKeepsPriority()
|
||||
{
|
||||
var item = Assert.Single(Document(NestJobDocument.CurrentVersion, 3).ToRequest(Drawings()).Items);
|
||||
|
||||
Assert.Equal(3, item.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionOneConvertsLowerFirstPriority()
|
||||
{
|
||||
var json = Document(1, 3).ToJson();
|
||||
|
||||
var item = Assert.Single(NestJobDocument.FromJson(json).ToRequest(Drawings()).Items);
|
||||
|
||||
Assert.Equal(-3, item.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownVersionIsRefused()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => Document(NestJobDocument.CurrentVersion + 1, 0).ToRequest(Drawings()));
|
||||
}
|
||||
|
||||
private static Drawing[] Drawings() => [new Drawing("a", TestDrawingFactory.Rectangle(10, 20))];
|
||||
|
||||
private static NestJobDocument Document(int version, int priority) => new()
|
||||
{
|
||||
Version = version,
|
||||
EngineName = "Rectangles",
|
||||
DrawingNames = ["a"],
|
||||
Items = [new NestJobDocument.ItemJson { Drawing = 0, Quantity = 1, Priority = priority }],
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class NestJobExampleTests
|
||||
new[]
|
||||
{
|
||||
Part("bracket", 100.0, 60.0, 5, priority: 0),
|
||||
Part("plate-clip", 40.0, 40.0, 8, priority: 1),
|
||||
Part("plate-clip", 40.0, 40.0, 8, priority: -1),
|
||||
},
|
||||
// Mixed inventory: five large sheets and unlimited small sheets.
|
||||
new[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class NestJobStockSelectionTests
|
||||
public void ExhaustedLargeStockIsNotRecreatedWhileSmallerStockServesSmallParts()
|
||||
{
|
||||
var result = Solve(
|
||||
new[] { Part("large", 1, 0), Part("small", 2, 1) },
|
||||
new[] { Part("large", 1, 1), Part("small", 2, 0) },
|
||||
new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 2) },
|
||||
request =>
|
||||
request.Stock.Id switch
|
||||
@@ -98,7 +98,7 @@ public class NestJobStockSelectionTests
|
||||
public void CandidatePriorityAreaEnvelopeAndInputOrderAreComparedInDocumentedOrder()
|
||||
{
|
||||
var priority = Solve(
|
||||
new[] { Part("high", 1, 0), Part("low", 1, 1) },
|
||||
new[] { Part("high", 1, 1), Part("low", 1, 0) },
|
||||
new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) },
|
||||
request =>
|
||||
request.Stock.Id == "a" ? Candidate(request, "low") : Candidate(request, "high")
|
||||
|
||||
@@ -28,7 +28,7 @@ public class RemoteNestProgressTests
|
||||
return new NestPipelineRequest(engine,
|
||||
[
|
||||
new NestItem { Drawing = new Drawing("bracket", TestDrawingFactory.Rectangle(10, 20)), Quantity = 9 },
|
||||
new NestItem { Drawing = new Drawing("ell", TestDrawingFactory.LShape()), Quantity = 6, Priority = 1 },
|
||||
new NestItem { Drawing = new Drawing("ell", TestDrawingFactory.LShape()), Quantity = 6, Priority = -1 },
|
||||
],
|
||||
NestStockBuilder.FromTemplate(template, null),
|
||||
new NestJobOptions(maxPlates: 5));
|
||||
|
||||
@@ -17,7 +17,7 @@ public class RemoteNestReplayTests
|
||||
var template = new Plate(48, 96) { EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25), PartSpacing = 0.25 };
|
||||
template.SetClampRail(new ClampRail(1.5, 0.625, new[] { 6.0, 30.0 }));
|
||||
return new NestPipelineRequest(engine,
|
||||
[new NestItem { Drawing = a, Quantity = 12 }, new NestItem { Drawing = b, Quantity = 7, Priority = 1 }],
|
||||
[new NestItem { Drawing = a, Quantity = 12 }, new NestItem { Drawing = b, Quantity = 7, Priority = -1 }],
|
||||
NestStockBuilder.FromTemplate(template,
|
||||
[new PlateOption { Width = 48, Length = 96, Cost = 100 }, new PlateOption { Width = 60, Length = 120, Quantity = 2, Cost = 160 }]),
|
||||
new NestJobOptions(maxPlates: 10, salvageRate: 0.5, minimumSalvageDimension: 6));
|
||||
|
||||
@@ -71,9 +71,9 @@ public abstract class EngineContractTests<TEngine> where TEngine : INestingEngin
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContractLowerNumberPriorityWins()
|
||||
public void ContractHigherNumberPriorityWins()
|
||||
{
|
||||
var job = Job([Part("low", Block(8), 1, priority: 9), Part("high", Block(8), 1, priority: 0)],
|
||||
var job = Job([Part("low", Block(8), 1, priority: 0), Part("high", Block(8), 1, priority: 9)],
|
||||
[Stock("s", 10, 10, quantity: 1)]);
|
||||
var result = new TEngine().Solve(job);
|
||||
LayoutAssert.Valid(job, result);
|
||||
|
||||
@@ -79,7 +79,7 @@ public abstract class RepeatEngineSafetyTests
|
||||
public void SameNamedDrawingsRetainExactDistinctRequirementIds()
|
||||
{
|
||||
var a = new Drawing("same", Shapes.Rectangle(6, 9));
|
||||
var b = new Drawing("same", Shapes.Rectangle(3, 9)) { Priority = 1 };
|
||||
var b = new Drawing("same", Shapes.Rectangle(3, 9)) { Priority = -1 };
|
||||
var job = Job([DrawingJobMapper.FromDrawing("a", a, 5), DrawingJobMapper.FromDrawing("A", b, 8)], [Stock("s", 10, 10, spacing: 0.2)]);
|
||||
var result = Engine().Solve(job);
|
||||
LayoutAssert.Valid(job, result);
|
||||
@@ -202,7 +202,7 @@ public abstract class RepeatEngineSafetyTests
|
||||
}
|
||||
|
||||
private static NestJob Mixed(int a, int b) => Job(
|
||||
[Part("a", Shapes.Rectangle(6, 9), a, RotationPolicy.Fixed(0)), Part("b", Shapes.Rectangle(3, 9), b, RotationPolicy.Fixed(0), priority: 1)],
|
||||
[Part("a", Shapes.Rectangle(6, 9), a, RotationPolicy.Fixed(0)), Part("b", Shapes.Rectangle(3, 9), b, RotationPolicy.Fixed(0), priority: -1)],
|
||||
[Stock("s", 10, 10, spacing: 0.2)]);
|
||||
|
||||
private sealed class InlineProgress(Action<NestJobProgress> report) : IProgress<NestJobProgress>
|
||||
|
||||
@@ -216,10 +216,10 @@ public class AreaPackerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_ServesLowerPriorityNumberFirst()
|
||||
public void Pack_ServesHigherPriorityNumberFirst()
|
||||
{
|
||||
var area = new Box(0, 0, 10, 10);
|
||||
var items = new List<NestItem> { Item("later", 10, 10, 1, priority: 1), Item("first", 6, 6, 1, priority: 0) };
|
||||
var items = new List<NestItem> { Item("later", 10, 10, 1, priority: 0), Item("first", 6, 6, 1, priority: 1) };
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 0, new DefaultFillComparer(), CancellationToken.None);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class NestJobCandidateComparer
|
||||
var priorities = parts
|
||||
.Select(part => part.Priority)
|
||||
.Distinct()
|
||||
.OrderBy(priority => priority);
|
||||
.OrderByDescending(priority => priority);
|
||||
foreach (var priority in priorities)
|
||||
{
|
||||
var leftCount = Count(left, priority);
|
||||
|
||||
@@ -9,8 +9,8 @@ public sealed class NestJobPart
|
||||
/// <param name="id">Unique requirement ID.</param>
|
||||
/// <param name="geometry">Snapshot of the part geometry.</param>
|
||||
/// <param name="quantity">Positive number requested.</param>
|
||||
/// <param name="priority">Placement precedence: lower numbers are placed first and win scarce
|
||||
/// stock. Zero is the default and highest ordinary priority; equal priorities are peers.</param>
|
||||
/// <param name="priority">Placement precedence: higher numbers are placed first and win scarce
|
||||
/// stock. Zero is the default; negative numbers place after it. Equal priorities are peers.</param>
|
||||
/// <param name="rotation">Allowed rotations, or null for automatic rotation.</param>
|
||||
public NestJobPart(
|
||||
string id,
|
||||
@@ -37,8 +37,8 @@ public sealed class NestJobPart
|
||||
/// <summary>Positive number requested; never decremented by placement code.</summary>
|
||||
public int Quantity { get; }
|
||||
/// <summary>
|
||||
/// Placement precedence: a lower number is placed first and wins scarce stock.
|
||||
/// Zero is the default and highest ordinary priority. Parts with equal priority are peers.
|
||||
/// Placement precedence: a higher number is placed first and wins scarce stock.
|
||||
/// Zero is the default; negative numbers place after it. Parts with equal priority are peers.
|
||||
/// </summary>
|
||||
public int Priority { get; }
|
||||
public RotationPolicy Rotation { get; }
|
||||
|
||||
@@ -36,7 +36,7 @@ internal static class PlateFillOrchestrator
|
||||
|
||||
var fillItems = items
|
||||
.Where(item => ShouldFill(item, plate, plateArea))
|
||||
.OrderBy(item => item.Priority)
|
||||
.OrderByDescending(item => item.Priority)
|
||||
.ThenByDescending(item => item.Drawing.Area)
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -517,7 +517,7 @@ internal class PlateFiller
|
||||
var workArea = Plate.WorkArea();
|
||||
var fillItems = items
|
||||
.Where(item => item.Quantity != 1)
|
||||
.OrderBy(item => item.Priority)
|
||||
.OrderByDescending(item => item.Priority)
|
||||
.ThenByDescending(item => item.Drawing.Area)
|
||||
.ToList();
|
||||
var packItems = items.Where(item => item.Quantity == 1).ToList();
|
||||
|
||||
@@ -226,7 +226,7 @@ public static class PlateFillService
|
||||
// strategies, partition the sheet at static keep-outs before searching, then
|
||||
// certify each accepted material footprint against the full zone and occupants.
|
||||
var result = new List<Part>();
|
||||
foreach (var item in items.OrderBy(item => item.Priority))
|
||||
foreach (var item in items.OrderByDescending(item => item.Priority))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Fill/Remnant whole-plate orchestration skips exhausted demand; the
|
||||
|
||||
@@ -15,7 +15,14 @@ namespace OpenNest.Engine.Jobs.Remote;
|
||||
/// </summary>
|
||||
public sealed class NestJobDocument
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
/// <summary>
|
||||
/// Version 2 nests higher priority numbers first. Version 1 jobs, from desktops that nested
|
||||
/// lower numbers first, still read, with their priorities converted
|
||||
/// (<see cref="LegacyPriority"/>); an older worker refuses version 2 instead of reversing it.
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 2;
|
||||
|
||||
private const int LowerFirstPriorityVersion = 1;
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
@@ -87,7 +94,7 @@ public sealed class NestJobDocument
|
||||
public NestPipelineRequest ToRequest(IReadOnlyList<Drawing> drawings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawings);
|
||||
if (Version != CurrentVersion)
|
||||
if (Version != CurrentVersion && Version != LowerFirstPriorityVersion)
|
||||
throw new FormatException($"Nest job version {Version} is not supported; this build reads {CurrentVersion}.");
|
||||
if (string.IsNullOrWhiteSpace(EngineName))
|
||||
throw new FormatException("The nest job names no engine.");
|
||||
@@ -108,7 +115,7 @@ public sealed class NestJobDocument
|
||||
{
|
||||
Drawing = drawings[item.Drawing],
|
||||
Quantity = item.Quantity,
|
||||
Priority = item.Priority,
|
||||
Priority = Version == LowerFirstPriorityVersion ? LegacyPriority.Convert(item.Priority) : item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace OpenNest.Engine
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public class NestItem
|
||||
{
|
||||
@@ -8,7 +8,7 @@
|
||||
public Drawing Drawing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Priority of the part determines nesting order. Highest priority will be nested first.
|
||||
/// Priority of the part determines nesting order. Higher numbers are nested first.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ internal enum DiscSeed
|
||||
/// <summary>
|
||||
/// Packs discs onto one sheet. Each disc goes to the best feasible center tangent to two of
|
||||
/// {work-area walls, placed discs, clamp keep-outs}, the exact candidate set for circles, so no
|
||||
/// no-fit polygons are needed. Discs go lower priority number first, then largest first, so small
|
||||
/// no-fit polygons are needed. Discs go higher priority number first, then largest first, so small
|
||||
/// discs land in the gaps between large ones. An optional hexagonal or square lattice of one type
|
||||
/// seeds the sheet before the greedy pass fills the rest.
|
||||
/// </summary>
|
||||
@@ -72,7 +72,7 @@ internal static class DiscPacker
|
||||
}
|
||||
}
|
||||
// Stable: equal radii keep requirement order.
|
||||
foreach (var type in fits.OrderBy(t => t.Priority).ThenByDescending(t => t.Radius))
|
||||
foreach (var type in fits.OrderByDescending(t => t.Priority).ThenByDescending(t => t.Radius))
|
||||
{
|
||||
while (left[type.Index] > 0)
|
||||
{
|
||||
@@ -102,7 +102,7 @@ internal static class DiscPacker
|
||||
{
|
||||
if (fits.Count == 0)
|
||||
return null;
|
||||
var top = fits.Min(t => t.Priority);
|
||||
var top = fits.Max(t => t.Priority);
|
||||
var best = fits.Where(t => t.Priority == top)
|
||||
.OrderByDescending(t => left[t.Index] * t.Radius * t.Radius)
|
||||
.First();
|
||||
|
||||
@@ -35,7 +35,7 @@ internal sealed record BarSheet(BarStock Bar, IReadOnlyList<BarPart> Parts, Box?
|
||||
/// and backed off until it passes; the box-to-box start always passes, so the result is never
|
||||
/// worse than plain box packing. Parts whose outline fills their box gain nothing and skip the
|
||||
/// slide. The length a slide frees at the end of a run is refilled from the remaining demand,
|
||||
/// lowest priority number first, then longest first.
|
||||
/// highest priority number first, then longest first.
|
||||
/// </summary>
|
||||
internal sealed class BarLayout
|
||||
{
|
||||
@@ -156,7 +156,7 @@ internal sealed class BarLayout
|
||||
|
||||
private IEnumerable<BarType> RefillOrder(BarStock bar, int[] available) =>
|
||||
types.Where(t => available[t.Index] > 0 && !double.IsNaN(bar.Lengths[t.Index]))
|
||||
.OrderBy(t => t.Priority)
|
||||
.OrderByDescending(t => t.Priority)
|
||||
.ThenByDescending(t => bar.Lengths[t.Index])
|
||||
.ThenBy(t => t.Index);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ internal sealed record BarPlan(IReadOnlyList<PackedBar> Bars, IReadOnlyList<BarI
|
||||
/// that replaces a shorter group in a bar with unplaced items that fill it better. DuplicateBins
|
||||
/// is omitted: it only acts on leftovers after the bar cap, when no bar can be added anyway.
|
||||
///
|
||||
/// OpenNest additions: items are ordered by priority before length, so a lower-number tier is
|
||||
/// OpenNest additions: items are ordered by priority before length, so a higher-number tier is
|
||||
/// packed first and wins scarce bars, and the swap step never trades an item for a later tier.
|
||||
/// The exhaustive search counts its nodes and stops at a fixed budget, keeping the best plan found.
|
||||
/// </summary>
|
||||
@@ -95,7 +95,7 @@ internal static class BarPacker
|
||||
{
|
||||
var remaining = items
|
||||
.Select((item, order) => (item, order))
|
||||
.OrderBy(x => x.item.Priority)
|
||||
.OrderByDescending(x => x.item.Priority)
|
||||
.ThenByDescending(x => x.item.Length)
|
||||
.ThenBy(x => x.item.Type)
|
||||
.ThenBy(x => x.order)
|
||||
@@ -135,7 +135,7 @@ internal static class BarPacker
|
||||
/// <summary>
|
||||
/// Takes out one item of a length group (never the longest group) and refills the freed
|
||||
/// space greedily from the unplaced items, keeping the swap only when the run ends fuller.
|
||||
/// Only items of the same or a lower-number priority may replace the removed one.
|
||||
/// Only items of the same or a higher-number priority may replace the removed one.
|
||||
/// </summary>
|
||||
private static bool TryImprove(PackedSegment segment, List<BarItem> remaining)
|
||||
{
|
||||
@@ -161,7 +161,7 @@ internal static class BarPacker
|
||||
var first = remaining[i];
|
||||
// Charging the trial its own gains is conservative for types already in the run.
|
||||
var trial = new PackedSegment(free);
|
||||
if (!trial.Fits(first) || first.Priority > removed.Priority)
|
||||
if (!trial.Fits(first) || first.Priority < removed.Priority)
|
||||
continue;
|
||||
trial.Items.Add(first);
|
||||
for (var j = i + 1; j < remaining.Count; j++)
|
||||
@@ -169,7 +169,7 @@ internal static class BarPacker
|
||||
if (trial.Remaining < shortest)
|
||||
break;
|
||||
var next = remaining[j];
|
||||
if (!trial.Fits(next) || next.Priority > removed.Priority)
|
||||
if (!trial.Fits(next) || next.Priority < removed.Priority)
|
||||
continue;
|
||||
trial.Items.Add(next);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ internal static class BarPacker
|
||||
var pool = items
|
||||
.GroupBy(i => i.Type)
|
||||
.Select(g => (Item: g.First(), Count: g.Count()))
|
||||
.OrderBy(t => t.Item.Priority)
|
||||
.OrderByDescending(t => t.Item.Priority)
|
||||
.ThenByDescending(t => t.Item.Length)
|
||||
.ThenBy(t => t.Item.Type)
|
||||
.ToList();
|
||||
@@ -392,7 +392,7 @@ internal static class BarPacker
|
||||
{
|
||||
var ordered = segment.Items
|
||||
.OrderByDescending(i => i.Length)
|
||||
.ThenBy(i => i.Priority)
|
||||
.ThenByDescending(i => i.Priority)
|
||||
.ThenBy(i => i.Type)
|
||||
.ToList();
|
||||
segment.Items.Clear();
|
||||
|
||||
@@ -245,14 +245,14 @@ public sealed class FlatBarNestingEngine : INestingEngine
|
||||
private readonly record struct Estimate(int Tier, int UrgentUnplaced, int Unplaced, double Cost)
|
||||
: IComparable<Estimate>
|
||||
{
|
||||
public static Estimate Zero => new(int.MaxValue, 0, 0, 0);
|
||||
public static Estimate Zero => new(int.MinValue, 0, 0, 0);
|
||||
|
||||
public static Estimate Leaving(IReadOnlyList<BarType> types, int[] left, double penalty)
|
||||
{
|
||||
var open = types.Where(t => left[t.Index] > 0).ToList();
|
||||
if (open.Count == 0)
|
||||
return Zero;
|
||||
var tier = open.Min(t => t.Priority);
|
||||
var tier = open.Max(t => t.Priority);
|
||||
var count = open.Sum(t => left[t.Index]);
|
||||
return new Estimate(tier, open.Where(t => t.Priority == tier).Sum(t => left[t.Index]), count, count * penalty);
|
||||
}
|
||||
@@ -261,9 +261,9 @@ public sealed class FlatBarNestingEngine : INestingEngine
|
||||
|
||||
public int CompareTo(Estimate other)
|
||||
{
|
||||
// A more urgent tier left unplaced is worse than any later tier left unplaced.
|
||||
// A more urgent (higher) tier left unplaced is worse than any later tier left unplaced.
|
||||
if (Tier != other.Tier)
|
||||
return other.Tier.CompareTo(Tier);
|
||||
return Tier.CompareTo(other.Tier);
|
||||
var c = UrgentUnplaced.CompareTo(other.UrgentUnplaced);
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
@@ -294,20 +294,20 @@ internal sealed class FrontierPacker
|
||||
var bestValue = double.PositiveInfinity;
|
||||
var bestSide = double.PositiveInfinity;
|
||||
var bestLead = double.PositiveInfinity;
|
||||
var bestPriority = int.MaxValue;
|
||||
var bestPriority = int.MinValue;
|
||||
var bestRank = int.MaxValue;
|
||||
|
||||
void Consider(Func<IReadOnlyList<Placed>> build, int typeIndex, double area, double advance, double side, double lead)
|
||||
{
|
||||
var priority = types[typeIndex].Part.Priority;
|
||||
var rank = groupRanks?[typeIndex] ?? 0;
|
||||
if (priority > bestPriority || (priority == bestPriority && rank > bestRank))
|
||||
if (priority < bestPriority || (priority == bestPriority && rank > bestRank))
|
||||
return;
|
||||
var fills = advance <= Tie;
|
||||
var value = PlacementValue(fills, advance, area, beta, typeWeights?[typeIndex] ?? 1.0);
|
||||
|
||||
var better = bestParts == null
|
||||
|| priority < bestPriority
|
||||
|| priority > bestPriority
|
||||
|| rank < bestRank
|
||||
|| (fills && !bestFills)
|
||||
|| (
|
||||
|
||||
@@ -29,8 +29,8 @@ internal sealed record SheetPlan(
|
||||
|
||||
/// <summary>
|
||||
/// Packs the remaining demand onto one sheet of the given stock with a maximal-rectangles free
|
||||
/// list. Lower priority numbers are always served first: a higher-number type is only placed
|
||||
/// when no lower-number type still fits anywhere.
|
||||
/// list. Higher priority numbers are always served first: a lower-number type is only placed
|
||||
/// when no higher-number type still fits anywhere.
|
||||
/// </summary>
|
||||
internal static class SheetPacker
|
||||
{
|
||||
|
||||
@@ -161,7 +161,7 @@ internal static class SheetByStock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Priority guard: how many instances of the most urgent (lowest-number) tier with remaining
|
||||
/// Priority guard: how many instances of the most urgent (highest-number) tier with remaining
|
||||
/// demand this layout leaves unplaced. Layouts are ranked on this before cost, so a cheaper sheet
|
||||
/// can never win by serving a later tier at the expense of an earlier one.
|
||||
/// </summary>
|
||||
@@ -171,7 +171,7 @@ internal static class SheetByStock
|
||||
var active = types.Where(t => remaining[t.Index] > 0).ToList();
|
||||
if (active.Count == 0)
|
||||
return 0;
|
||||
var top = active.Min(t => t.Priority);
|
||||
var top = active.Max(t => t.Priority);
|
||||
var placed = layout.Poses.Count(p => priority[p.PartId] == top);
|
||||
return active.Where(t => t.Priority == top).Sum(t => remaining[t.Index]) - placed;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace OpenNest.Engine.RectanglePacking;
|
||||
/// <summary>
|
||||
/// Packs mixed parts into one rectangular area as their program bounding boxes, at 0 or 90
|
||||
/// degrees, with the maximal-rectangles packer. Every fit rule and pick mode is tried. The layout
|
||||
/// that places the most parts, then the most box area, wins, compared tier by tier from the lowest
|
||||
/// that places the most parts, then the most box area, wins, compared tier by tier from the highest
|
||||
/// <see cref="NestItem.Priority"/>; the caller's fill comparer breaks ties, so leftover packing
|
||||
/// keeps the same goal (density, or a clear right or top remnant) as the rest of the fill.
|
||||
/// </summary>
|
||||
@@ -66,7 +66,7 @@ internal static class AreaPacker
|
||||
demand[i] = System.Math.Clamp(items[i].Quantity, 0, capacity);
|
||||
}
|
||||
|
||||
var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray();
|
||||
var tiers = types.Select(t => t.Priority).Distinct().OrderDescending().ToArray();
|
||||
List<Part>? best = null;
|
||||
(int Count, double Area)[]? bestTiers = null;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ internal enum PickMode
|
||||
}
|
||||
|
||||
/// <summary>A box type to pack.</summary>
|
||||
/// <param name="Priority">Demand tier; lower numbers are served first.</param>
|
||||
/// <param name="Priority">Demand tier; higher numbers are served first.</param>
|
||||
/// <param name="Sizes">Allowed packing sizes (one per orientation), already grown by any spacing.</param>
|
||||
/// <param name="Area">Ranking area: larger boxes win score ties and go first in ordered mode.</param>
|
||||
internal sealed record PackType(int Priority, IReadOnlyList<(double Width, double Height)> Sizes, double Area);
|
||||
@@ -25,9 +25,9 @@ internal sealed record PackType(int Priority, IReadOnlyList<(double Width, doubl
|
||||
internal readonly record struct PackPlacement(int Type, int Size, double X, double Y);
|
||||
|
||||
/// <summary>
|
||||
/// Packs box types onto one <see cref="MaxRectsSheet"/> under a fit rule and pick mode. Lower
|
||||
/// priority numbers are always served first: a higher-number type is only placed when no
|
||||
/// lower-number type still fits anywhere. Deterministic: ties resolve by input order.
|
||||
/// Packs box types onto one <see cref="MaxRectsSheet"/> under a fit rule and pick mode. Higher
|
||||
/// priority numbers are always served first: a lower-number type is only placed when no
|
||||
/// higher-number type still fits anywhere. Deterministic: ties resolve by input order.
|
||||
/// </summary>
|
||||
internal static class MaxRectsPacker
|
||||
{
|
||||
@@ -60,7 +60,7 @@ internal static class MaxRectsPacker
|
||||
{
|
||||
// Free space only shrinks, so a size that fails once never fits again on this sheet.
|
||||
var dead = types.Select(t => new bool[t.Sizes.Count]).ToArray();
|
||||
var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray();
|
||||
var tiers = types.Select(t => t.Priority).Distinct().OrderDescending().ToArray();
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ internal static class MaxRectsPacker
|
||||
var order = Enumerable
|
||||
.Range(0, types.Count)
|
||||
.Where(t => types[t].Sizes.Count > 0)
|
||||
.OrderBy(t => types[t].Priority)
|
||||
.OrderByDescending(t => types[t].Priority)
|
||||
.ThenByDescending(t => types[t].Area)
|
||||
.ThenBy(t => t);
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace OpenNest.IO.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Priority nests higher numbers first. Files keep writing the lower-first legacy value under
|
||||
/// <c>priority</c> so older readers keep the same order, and files without <c>nestPriority</c>
|
||||
/// are converted when read.
|
||||
/// </summary>
|
||||
public class NestPriorityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Write_KeepsTheLegacyValueBesideTheNewOne()
|
||||
{
|
||||
var file = Write(5);
|
||||
|
||||
var drawing = DrawingJson(file);
|
||||
Assert.Equal(-5, (int)drawing["priority"]!);
|
||||
Assert.Equal(5, (int)drawing["nestPriority"]!);
|
||||
Assert.Equal(5, Read(file).Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_OmitsAZeroPriority()
|
||||
{
|
||||
var file = Write(0);
|
||||
|
||||
Assert.False(DrawingJson(file).ContainsKey("nestPriority"));
|
||||
Assert.Equal(0, Read(file).Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_ConvertsAFileWithOnlyTheLegacyValue()
|
||||
{
|
||||
var file = Rewrite(Write(5), drawing =>
|
||||
{
|
||||
drawing.Remove("nestPriority");
|
||||
drawing["priority"] = 3;
|
||||
});
|
||||
|
||||
Assert.Equal(-3, Read(file).Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_PrefersTheNewValue()
|
||||
{
|
||||
var file = Rewrite(Write(5), drawing => drawing["priority"] = 9);
|
||||
|
||||
Assert.Equal(5, Read(file).Priority);
|
||||
}
|
||||
|
||||
private static byte[] Write(int priority)
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
drawing.Priority = priority;
|
||||
var nest = new Nest(drawing.Name) { Units = Units.Millimeters };
|
||||
nest.Drawings.Add(drawing);
|
||||
using var stream = new MemoryStream();
|
||||
Assert.True(new NestWriter(nest).Write(stream));
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static Drawing Read(byte[] file) =>
|
||||
Assert.Single(new NestReader(new MemoryStream(file)).Read().Drawings);
|
||||
|
||||
private static JsonObject DrawingJson(byte[] file)
|
||||
{
|
||||
using var archive = new ZipArchive(new MemoryStream(file), ZipArchiveMode.Read);
|
||||
using var reader = new StreamReader(archive.GetEntry("nest.json")!.Open(), Encoding.UTF8);
|
||||
return JsonNode.Parse(reader.ReadToEnd())!["drawings"]![0]!.AsObject();
|
||||
}
|
||||
|
||||
private static byte[] Rewrite(byte[] file, Action<JsonObject> change)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
stream.Write(file);
|
||||
using (var archive = new ZipArchive(stream, ZipArchiveMode.Update, leaveOpen: true))
|
||||
{
|
||||
var entry = archive.GetEntry("nest.json")!;
|
||||
JsonNode root;
|
||||
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
|
||||
root = JsonNode.Parse(reader.ReadToEnd())!;
|
||||
change(root["drawings"]![0]!.AsObject());
|
||||
entry.Delete();
|
||||
using var writer = new StreamWriter(archive.CreateEntry("nest.json").Open(), new UTF8Encoding(false));
|
||||
writer.Write(root.ToJsonString());
|
||||
}
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace OpenNest.IO
|
||||
private const string FixedDate = "2000-01-01T00:00:00.0000000";
|
||||
|
||||
// Removed so readers fall back to their defaults; the library record owns these values.
|
||||
private static readonly string[] RecordOwnedDrawingFields = { "customer", "color", "customColor", "priority", "cutFirst", "constraints" };
|
||||
private static readonly string[] RecordOwnedDrawingFields = { "customer", "color", "customColor", "priority", "nestPriority", "cutFirst", "constraints" };
|
||||
|
||||
/// <summary>Packages <paramref name="drawing"/> without changing it.</summary>
|
||||
public static byte[] Write(Drawing drawing, Units units)
|
||||
|
||||
@@ -443,8 +443,18 @@ namespace OpenNest.IO
|
||||
public bool CustomColor { get; init; }
|
||||
|
||||
public QuantityDto Quantity { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The lower-first legacy priority (<see cref="OpenNest.LegacyPriority"/>), still
|
||||
/// written so older readers keep the same order. Read only when
|
||||
/// <see cref="NestPriority"/> is absent.
|
||||
/// </summary>
|
||||
public int Priority { get; init; }
|
||||
|
||||
/// <summary>Nesting priority, higher numbers first. Omitted when zero.</summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? NestPriority { get; init; }
|
||||
|
||||
/// <summary>Sequence this drawing's parts first. Omitted when false.</summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public bool CutFirst { get; init; }
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace OpenNest.IO
|
||||
drawing.Color = Color.FromArgb(d.Color.A, d.Color.R, d.Color.G, d.Color.B);
|
||||
drawing.HasCustomColor = d.CustomColor;
|
||||
drawing.Quantity.Required = d.Quantity.Required;
|
||||
drawing.Priority = d.Priority;
|
||||
drawing.Priority = d.NestPriority ?? LegacyPriority.Convert(d.Priority);
|
||||
drawing.CutFirst = d.CutFirst;
|
||||
drawing.Constraints.StepAngle = d.Constraints.StepAngle;
|
||||
drawing.Constraints.StartAngle = d.Constraints.StartAngle;
|
||||
|
||||
@@ -236,7 +236,8 @@ namespace OpenNest.IO
|
||||
},
|
||||
CustomColor = d.HasCustomColor,
|
||||
Quantity = new QuantityDto { Required = d.Quantity.Required },
|
||||
Priority = d.Priority,
|
||||
Priority = LegacyPriority.Convert(d.Priority),
|
||||
NestPriority = d.Priority == 0 ? null : d.Priority,
|
||||
CutFirst = d.CutFirst,
|
||||
Constraints = new ConstraintsDto
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
Assert.Empty(database.QueryDrawings(new DrawingLibraryQuery { IncludeArchived = true }).Items);
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
@@ -44,7 +44,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
{
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
// Two distinct blobs across four revisions: the bracket's notes edit and the
|
||||
// plate both reuse the bracket's first package.
|
||||
@@ -81,11 +81,37 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using (new NestDatabase(factory.DatabasePath, pooling: false)) { }
|
||||
Execute(factory.DatabasePath, "PRAGMA user_version=18");
|
||||
Execute(factory.DatabasePath, "PRAGMA user_version=19");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => new NestDatabase(factory.DatabasePath, pooling: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void V18Upgrade_NegatesLowerFirstPrioritiesInEveryRevision()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
var record = Record();
|
||||
var other = Record("Synthetic plate");
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
{
|
||||
database.InsertDrawing(record.Id, record, Package());
|
||||
database.InsertDrawing(other.Id, other, Package("other"));
|
||||
}
|
||||
// A v17 library stored lower-first values: 3 nested before 0, and int.MinValue before both.
|
||||
Execute(factory.DatabasePath, $"""
|
||||
UPDATE drawings SET priority = 3 WHERE id = '{record.Id}';
|
||||
UPDATE drawing_revisions SET priority = 3 WHERE id = '{record.Id}';
|
||||
UPDATE drawings SET priority = -2147483648 WHERE id = '{other.Id}';
|
||||
PRAGMA user_version=17;
|
||||
""");
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(-3, upgraded.GetDrawing(record.Id)!.Settings.Priority);
|
||||
Assert.Equal(-3, Assert.Single(upgraded.DrawingHistory(record.Id, 0, 100)!.Items).Settings.Priority);
|
||||
Assert.Equal(int.MaxValue, upgraded.GetDrawing(other.Id)!.Settings.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedDrawingMigration_RollsBackAndCanRestart()
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
Assert.Empty(database.QueryDrawingSets(new DrawingSetQuery { IncludeArchived = true }).Items);
|
||||
Assert.Equal(before, Digest(factory.DatabasePath));
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
@@ -48,7 +48,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
Assert.Equal(before, Digest(factory.DatabasePath));
|
||||
Execute(factory.DatabasePath, "DROP TABLE ix_drawing_set_members_drawing");
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class MaterialListTests
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Empty(upgraded.ListMaterials());
|
||||
Assert.Equal((" Stainless ", "304"), (upgraded.Get(nest)!.Material, upgraded.Get(nest)!.MaterialGrade));
|
||||
Assert.Equal(drawing.MaterialName, upgraded.GetDrawing(drawing.Id)!.MaterialName);
|
||||
|
||||
@@ -75,7 +75,7 @@ public sealed class NestMaterialGradeTests
|
||||
Assert.Equal("Stainless", record.Material);
|
||||
Assert.Equal("", record.MaterialGrade);
|
||||
Assert.Equal("", Assert.Single(upgraded.History(id, 0, 10)!.Items).MaterialGrade);
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
private static NestRecord Record(string name, string material, string grade) => new()
|
||||
|
||||
@@ -119,7 +119,7 @@ public sealed class NestStockCodesTests
|
||||
database.SetDeleted(deleted, false, record.Revision);
|
||||
Assert.Equal(["SHEET-1", "SHEET-2"], database.StockCodesInUse(null).Select(u => u.ItemCode));
|
||||
}
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
private static Nest Nest(string name, NestStatus status, params string[][] plateCodes)
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed class NestUnitsTests
|
||||
Assert.Equal([Units.Millimeters, Units.Millimeters], upgraded.History(metric, 0, 10)!.Items.Select(r => r.Units));
|
||||
Assert.Equal(Units.Inches, upgraded.Get(inch)!.Units);
|
||||
Assert.Equal(Units.Inches, upgraded.Get(unreadable)!.Units);
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
private static NestRecord Record(string name, Units units) => new()
|
||||
|
||||
@@ -152,7 +152,7 @@ public sealed class SetReleaseDrawingTests
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
var kept = upgraded.GetRelease(releaseId)!;
|
||||
Assert.Empty(kept.Drawings);
|
||||
Assert.Equal(((Guid?)kit.Id, drawing, 2), (kept.Nests[0].Items.Single().SetId, kept.Nests[0].Items.Single().DrawingId, kept.Nests[0].Items.Single().Quantity));
|
||||
|
||||
@@ -295,7 +295,7 @@ public sealed class SetReleaseTests
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'set_release%' OR name LIKE 'ix_set_release%'"));
|
||||
var release = upgraded.GetRelease(releaseId)!;
|
||||
Assert.Equal(("Legacy notes", "Legacy operator", false), (release.Notes, release.ReleasedBy, release.IsUndone));
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class SharedSettingsTests
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
|
||||
Assert.Equal(17L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(18L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Empty(upgraded.ListSharedSettings());
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,16 @@ public sealed partial class NestDatabase
|
||||
PRAGMA user_version=5;
|
||||
""";
|
||||
|
||||
// v18: priority nests higher numbers first. Stored values were lower-first, so they are
|
||||
// negated as LegacyPriority.Convert does (int.MinValue, which has no negation, becomes int.MaxValue).
|
||||
private const string PriorityNegation = "CASE WHEN priority = -2147483648 THEN 2147483647 ELSE -priority END";
|
||||
|
||||
private const string DrawingPriorityUpgradeToV18 = $"""
|
||||
UPDATE drawings SET priority = {PriorityNegation} WHERE priority <> 0;
|
||||
UPDATE drawing_revisions SET priority = {PriorityNegation} WHERE priority <> 0;
|
||||
PRAGMA user_version=18;
|
||||
""";
|
||||
|
||||
private const string DrawingColumns = """
|
||||
id, revision, name, customer, materialName, materialGrade, thickness, units,
|
||||
cutType, notes, archived, packageSha256, packageSize, savedAt, savedBy,
|
||||
|
||||
@@ -344,7 +344,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "PRAGMA user_version";
|
||||
var version = (long)command.ExecuteScalar()!;
|
||||
if (version > 17) throw new InvalidOperationException("Database schema is newer than this server.");
|
||||
if (version > 18) throw new InvalidOperationException("Database schema is newer than this server.");
|
||||
if (version == 0)
|
||||
{
|
||||
command.CommandText = """
|
||||
@@ -449,6 +449,12 @@ public sealed partial class NestDatabase : IDisposable
|
||||
AddJobProgress(command);
|
||||
if (version < 17)
|
||||
AddJobReview(command);
|
||||
if (version < 18)
|
||||
{
|
||||
// Drawing priority now nests higher numbers first; negating keeps every order.
|
||||
command.CommandText = DrawingPriorityUpgradeToV18;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text.Json;
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Library settings carry priority (higher first) as <c>nestPriority</c> and keep the
|
||||
/// lower-first legacy value under <c>priority</c>, which older clients read and send.
|
||||
/// </summary>
|
||||
public class DrawingNestingSettingsPriorityTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web);
|
||||
|
||||
[Fact]
|
||||
public void WritesTheLegacyValueBeforeTheNewOne()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new DrawingNestingSettings { Priority = 4 }, Web);
|
||||
|
||||
Assert.Contains("\"priority\":-4,\"nestPriority\":4", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadsAnOlderClientsLegacyValue()
|
||||
{
|
||||
var settings = JsonSerializer.Deserialize<DrawingNestingSettings>("""{"cutFirst":true,"priority":2}""", Web)!;
|
||||
|
||||
Assert.Equal(-2, settings.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTripsAndStaysEqual()
|
||||
{
|
||||
var settings = new DrawingNestingSettings { Priority = 7, CutFirst = true };
|
||||
|
||||
var back = JsonSerializer.Deserialize<DrawingNestingSettings>(JsonSerializer.Serialize(settings, Web), Web);
|
||||
|
||||
Assert.Equal(settings, back);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public class RemoteDrawingLibraryTests
|
||||
|
||||
Assert.Equal(HttpMethod.Put, handler.Requests[0].Method);
|
||||
Assert.EndsWith($"/api/drawings/{record.Id}/metadata", handler.Requests[0].RequestUri!.AbsoluteUri);
|
||||
Assert.Contains("\"settings\":{\"cutFirst\":true,\"priority\":2", handler.RequestBodies[0]);
|
||||
Assert.Contains("\"settings\":{\"cutFirst\":true,\"priority\":-2,\"nestPriority\":2", handler.RequestBodies[0]);
|
||||
Assert.Equal(record.Settings, saved.Settings);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -406,6 +406,7 @@ namespace OpenNest.Forms
|
||||
priorityBox.Dock = DockStyle.Fill;
|
||||
priorityBox.Margin = new Padding(3, 4, 3, 4);
|
||||
priorityBox.Maximum = new decimal(new int[] { 999999, 0, 0, 0 });
|
||||
priorityBox.Minimum = new decimal(new int[] { 999999, 0, 0, int.MinValue });
|
||||
priorityBox.Name = "priorityBox";
|
||||
priorityBox.Suffix = "";
|
||||
priorityBox.TabIndex = 1;
|
||||
@@ -416,7 +417,7 @@ namespace OpenNest.Forms
|
||||
priorityHint.AutoSize = true;
|
||||
priorityHint.ForeColor = SystemColors.GrayText;
|
||||
priorityHint.Name = "priorityHint";
|
||||
priorityHint.Text = "Lower numbers nest first";
|
||||
priorityHint.Text = "Higher numbers nest first";
|
||||
//
|
||||
// cutFirstBox
|
||||
//
|
||||
|
||||
@@ -323,7 +323,7 @@ the server's library record owns are written as their defaults, so editing them
|
||||
changes the hash:
|
||||
|
||||
- nest and drawing `name` (blank), drawing `customer`, material `name` and `grade` (blank);
|
||||
- `color`, `customColor`, `priority`, `cutFirst` and `constraints` (omitted);
|
||||
- `color`, `customColor`, `priority`, `nestPriority`, `cutFirst` and `constraints` (omitted);
|
||||
- `quantity.required` (zero) and `library` (omitted).
|
||||
|
||||
Adding a library drawing to a nest ignores whatever a package holds for these fields
|
||||
@@ -346,6 +346,14 @@ from the active scheme's palette, so the view follows the application's scheme
|
||||
rather than whichever scheme the file was saved under. Files written before this
|
||||
flag have no custom colors and are recolored on open. Split pieces keep the flag.
|
||||
|
||||
## Drawing priority
|
||||
|
||||
`nestPriority` is the drawing's nesting priority: higher numbers nest first, and it is
|
||||
omitted when zero. Readers that predate it used `priority`, where lower numbers nested
|
||||
first, so writers keep writing `priority` as the negated value and older readers keep the
|
||||
same order. A reader uses `nestPriority` when present and otherwise negates `priority`, so
|
||||
files written before the change open in the order they were saved with.
|
||||
|
||||
## Drawing cut-first flag
|
||||
|
||||
A drawing may write `cutFirst: true` to have automatic part sequencing cut its parts
|
||||
|
||||
@@ -307,7 +307,9 @@ first upload), `revision`, `name`, `customer`, `materialName`, `materialGrade`,
|
||||
and `savedAt`.
|
||||
|
||||
`settings` holds the drawing's nesting settings, applied when it is added to a
|
||||
nest: `cutFirst` (sequence its parts first, for example test squares), `priority`,
|
||||
nest: `cutFirst` (sequence its parts first, for example test squares), `nestPriority`
|
||||
(higher numbers nest first; `priority` carries the negated, lower-first value for older
|
||||
clients, and a request without `nestPriority` is read from it),
|
||||
`stepAngle`, `startAngle`, `endAngle` (radians), `allow180Equivalent` and
|
||||
`colorArgb` (32-bit ARGB; alpha 0 means no stored color, so the nest assigns one).
|
||||
They are record fields, not package content (see
|
||||
|
||||
@@ -241,7 +241,7 @@ its length less that gain; the first copy of each run pays the gain back.
|
||||
Bar by bar, every available stock offers two candidate bars: the fullest bar of the CutList packer
|
||||
(first-fit decreasing plus its swap optimization, or an exhaustive fewest-bars search for 25 items
|
||||
or fewer) and the fullest single bar a bounded subset-sum search can fill from the remaining
|
||||
demand. Lower priority numbers pack first in both. Each candidate is laid out, then the rest of
|
||||
demand. Higher priority numbers pack first in both. Each candidate is laid out, then the rest of
|
||||
the demand is priced at the cheaper of each stock's cut-list plan and its repeated best fills. The
|
||||
winner has the fewest unplaced parts of the most urgent tier, then the fewest unplaced parts, then
|
||||
the lowest salvage-credited cost. Searches stop at counted node budgets, so results are
|
||||
@@ -345,7 +345,7 @@ that circle (a square fills 0.64, a gear about 0.85), it returns everything unpl
|
||||
reporting progress, and Default discards it at almost no cost. Holes are not used here; rings in
|
||||
rings come from the cutout pre-pass, which hands the engine each nested chain as its outer frame.
|
||||
|
||||
Each disc goes, lower priority number first and then largest first, to the best feasible center
|
||||
Each disc goes, higher priority number first and then largest first, to the best feasible center
|
||||
tangent to two of: the work-area walls, placed discs, and clamp zones grown by the clamp
|
||||
clearance (square-cornered, as the layout check grows them). Discs keep the part spacing plus
|
||||
twice `NestTolerances.ValidationOutline` between them. Bottom-left fills rows and left-bottom
|
||||
|
||||
@@ -98,7 +98,8 @@ package and result on the server.
|
||||
|
||||
**The package** (`OpenNest.Api.NestJobPackage`) is a ZIP of:
|
||||
|
||||
- `job.json`: `NestJobDocument`: engine name, each item's quantity, priority, rotation range
|
||||
- `job.json`: `NestJobDocument`: engine name, each item's quantity, priority (higher first;
|
||||
version 1 jobs nested lower numbers first and are converted when read), rotation range
|
||||
and drawing index, the drawing names in order (a guard against reordering), each drawing's
|
||||
geometry fingerprint (a SHA-256 of the exact motions the freshness check compares, which a
|
||||
later Open checks the nest's drawings against; workers ignore it), every stock
|
||||
|
||||
Reference in new issue
Block a user