mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 14:55:45 -05:00
Slice 3 of common-line cutting. On costly plate (N26-295: 3/16 RA310S at about $2,500 a sheet) pairing parts one kerf apart fits the part the spacing cannot: the last plate's 38 parts fit on one 48x120 with Auto Nest only with pairs. - Drawing Properties gains "Allow common line" (Drawing.AllowCommonLine, off by default because customer edge-quality needs vary). The nest's CommonLineUsage (Off / When it adds parts / Whenever available) is set in Auto Nest and seeded from Tools > Nest Defaults; both persist (nest.json, defaults.json). - NestPipeline runs the ordinary solve, then, with a level on, trials where eligible parts are offered as kerf-gap pair compounds. Placed pairs expand into parts plus explicit NestJobCommonLine groups, are validated again, and CommonLineRanking keeps a trial only by the level: count first, then sheets; "when it adds parts" keeps the plain layout on a tie, "whenever" also takes a shorter cut or less material, never at the cost of a part. Engines, FillScore and their comparers are unchanged; Off asks the engine exactly what it did. - NestLayoutCheck exempts members only through a sheet's explicit group list and only when the job allows common lines; materializing and committing create Plate.CommonLines. - Server nesting writes common-line jobs as version 3 and replays the recorded trial answers, so the desktop binds the same groups. - Fill Area honors the level through a PlateFillService overload. - The benchmark gains --common-lines <level> --kerf <k>. Benchmark (synthetic nests + N26-295, --parallel 1): with no flag and with --common-lines off every engine matches the base commit; with a level on no layout becomes invalid and three runs save a sheet at equal nested area. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
5.6 KiB
C#
121 lines
5.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace OpenNest.Engine.Jobs.Remote;
|
|
|
|
/// <summary>
|
|
/// Every engine solve one <see cref="NestPipeline.Run(INestingEngine, string, NestPipelineRequest, IProgress{NestJobProgress}, System.Threading.CancellationToken)"/>
|
|
/// made, in order: what it asked (part and stock ids with their quantities) and the raw result
|
|
/// the engine returned. A remote solve sends this back instead of bound plates, so the client
|
|
/// replays the pipeline against its own drawings and validates and binds the result itself
|
|
/// (<see cref="RemoteNestSubmission"/>). Poses are kept bit for bit, nonfinite ones included,
|
|
/// so the client's validation sees exactly what the engine returned.
|
|
/// </summary>
|
|
public sealed class NestSolveTranscript
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
internal static readonly JsonSerializerOptions Options = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
PropertyNameCaseInsensitive = true,
|
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
|
|
};
|
|
|
|
public int Version { get; set; } = CurrentVersion;
|
|
public List<SolveJson> Solves { get; set; } = new();
|
|
|
|
public string ToJson() => JsonSerializer.Serialize(this, Options);
|
|
|
|
/// <summary>Reads a transcript; throws <see cref="FormatException"/> when it is not one this build understands.</summary>
|
|
public static NestSolveTranscript FromJson(string json)
|
|
{
|
|
NestSolveTranscript transcript;
|
|
try { transcript = JsonSerializer.Deserialize<NestSolveTranscript>(json, Options); }
|
|
catch (JsonException ex) { throw new FormatException("The server's nest result is not valid JSON.", ex); }
|
|
if (transcript?.Solves == null || transcript.Solves.Any(s => s?.Result == null))
|
|
throw new FormatException("The server's nest result is incomplete.");
|
|
if (transcript.Version != CurrentVersion)
|
|
throw new FormatException($"The server's nest result is version {transcript.Version}; this build reads {CurrentVersion}.");
|
|
return transcript;
|
|
}
|
|
|
|
internal void Add(NestJob job, NestJobResult result) =>
|
|
Solves.Add(new SolveJson { Request = Fingerprint(job), Result = ResultJson.From(result) });
|
|
|
|
/// <summary>What a solve was asked: the job's part and stock ids with their quantities, in order.</summary>
|
|
internal static List<string> Fingerprint(NestJob job) =>
|
|
job.Parts.Select(p => $"part:{p.Id}:{p.Quantity}")
|
|
.Concat(job.Plates.Select(s => $"stock:{s.Id}:{s.Quantity?.ToString() ?? "*"}"))
|
|
.ToList();
|
|
|
|
public sealed class SolveJson
|
|
{
|
|
public List<string> Request { get; set; } = new();
|
|
public ResultJson Result { get; set; }
|
|
}
|
|
|
|
public sealed class ResultJson
|
|
{
|
|
public NestJobStatus Status { get; set; }
|
|
public NestJobStopReason StopReason { get; set; }
|
|
public List<PlateJson> Plates { get; set; } = new();
|
|
public List<PartFulfillment> Fulfillment { get; set; } = new();
|
|
public List<StockUsage> StockUsage { get; set; } = new();
|
|
|
|
internal static ResultJson From(NestJobResult result) => new()
|
|
{
|
|
Status = result.Status,
|
|
StopReason = result.StopReason,
|
|
Plates = result.Plates.Select(p => new PlateJson
|
|
{
|
|
PlateIndex = p.PlateIndex,
|
|
Stock = NestStockJson.From(p.Stock),
|
|
Placements = p.Placements.ToList(),
|
|
CommonLines = p.CommonLines.Count == 0 ? null
|
|
: p.CommonLines.Select(g => new CommonLineJson { Kerf = g.Kerf, Placements = g.Placements.ToList() }).ToList(),
|
|
}).ToList(),
|
|
Fulfillment = result.Fulfillment.ToList(),
|
|
StockUsage = result.StockUsage.ToList(),
|
|
};
|
|
|
|
/// <summary>The raw result, with each sheet that matches one of <paramref name="job"/>'s
|
|
/// stock entries using that entry itself, as an engine solving the job would.</summary>
|
|
internal NestJobResult ToResult(NestJob job)
|
|
{
|
|
var plates = new List<NestJobPlateResult>(Plates.Count);
|
|
foreach (var plate in Plates)
|
|
{
|
|
var stock = plate.Stock?.ToStock() ?? throw new FormatException("A result sheet has no stock.");
|
|
var own = job.Plates.FirstOrDefault(s => RepeatedPlateGrouping.SameStock(s, stock));
|
|
plates.Add(new NestJobPlateResult(plate.PlateIndex, own ?? stock, plate.Placements ?? new(),
|
|
plate.CommonLines?.Select(g => g == null ? throw new FormatException("A result common-line group is empty.")
|
|
: new NestJobCommonLine(g.Kerf, g.Placements ?? new()))));
|
|
}
|
|
return new NestJobResult(Status, StopReason, plates, Fulfillment ?? new(), StockUsage ?? new());
|
|
}
|
|
}
|
|
|
|
public sealed class PlateJson
|
|
{
|
|
public int PlateIndex { get; set; }
|
|
public NestStockJson Stock { get; set; }
|
|
public List<NestJobPlacement> Placements { get; set; } = new();
|
|
|
|
/// <summary>Common-line groups an engine returned itself (none of the built-in engines
|
|
/// do: the pipeline makes pairs around them). Omitted when empty.</summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public List<CommonLineJson> CommonLines { get; set; }
|
|
}
|
|
|
|
public sealed class CommonLineJson
|
|
{
|
|
public double Kerf { get; set; }
|
|
public List<int> Placements { get; set; } = new();
|
|
}
|
|
}
|