mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 18:05:44 -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>
156 lines
5.9 KiB
C#
156 lines
5.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using OpenNest.Engine.Jobs.CommonLines;
|
|
using OpenNest.Engine.Jobs.Cutouts;
|
|
|
|
namespace OpenNest.Engine.Jobs.Remote;
|
|
|
|
/// <summary>
|
|
/// The nest server worker's latest progress report for a running job: the stage of the solve
|
|
/// in progress, without its plan (plans travel as <see cref="RemoteNestPlan"/>). Committed
|
|
/// counts follow <see cref="NestJobProgress"/>: authoritative only after a commit.
|
|
/// </summary>
|
|
public sealed class RemoteNestProgress
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
public int Version { get; set; } = CurrentVersion;
|
|
public NestJobStage Stage { get; set; }
|
|
public string StockId { get; set; } = "";
|
|
public int PlateIndex { get; set; }
|
|
public int CommittedPlates { get; set; }
|
|
public int CommittedParts { get; set; }
|
|
|
|
public static RemoteNestProgress From(NestJobProgress progress)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(progress);
|
|
return new RemoteNestProgress
|
|
{
|
|
Stage = progress.Stage,
|
|
StockId = progress.StockId ?? "",
|
|
PlateIndex = progress.PlateIndex,
|
|
CommittedPlates = progress.CommittedPlates,
|
|
CommittedParts = progress.CommittedParts,
|
|
};
|
|
}
|
|
|
|
public NestJobProgress ToProgress() => new(Stage, StockId, PlateIndex, CommittedPlates, CommittedParts);
|
|
|
|
public string ToJson() => JsonSerializer.Serialize(this, NestSolveTranscript.Options);
|
|
|
|
/// <summary>The report, or null when <paramref name="json"/> is empty or not one this build reads.</summary>
|
|
public static RemoteNestProgress FromJson(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return null;
|
|
try
|
|
{
|
|
var progress = JsonSerializer.Deserialize<RemoteNestProgress>(json, NestSolveTranscript.Options);
|
|
return progress?.Version == CurrentVersion ? progress : null;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The engine's best whole-job plan so far on the worker (<see cref="NestJobStage.PlanImproved"/>),
|
|
/// as raw placements. The desktop draws it with <see cref="RemoteNestPlanPreview"/>; like a local
|
|
/// plan it is unvalidated and never kept.
|
|
/// </summary>
|
|
public sealed class RemoteNestPlan
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
public int Version { get; set; } = CurrentVersion;
|
|
public NestSolveTranscript.ResultJson Plan { get; set; }
|
|
|
|
public static RemoteNestPlan From(NestJobResult plan)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(plan);
|
|
return new RemoteNestPlan { Plan = NestSolveTranscript.ResultJson.From(plan) };
|
|
}
|
|
|
|
public string ToJson() => JsonSerializer.Serialize(this, NestSolveTranscript.Options);
|
|
|
|
/// <summary>The plan, or null when <paramref name="json"/> is not one this build reads.</summary>
|
|
public static RemoteNestPlan FromJson(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return null;
|
|
try
|
|
{
|
|
var plan = JsonSerializer.Deserialize<RemoteNestPlan>(json, NestSolveTranscript.Options);
|
|
return plan?.Version == CurrentVersion && plan.Plan?.Plates != null ? plan : null;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the worker's best plan so far on the desktop, the way a local run's
|
|
/// <see cref="NestJobProgress.PlanSheets"/> are drawn: the request's job is rebuilt here (the
|
|
/// same part ids the worker solved), and parts go on detached drawings built from its geometry,
|
|
/// so nothing touches the caller's drawings or quantities. A bundled cutout trial's frame proxies
|
|
/// are bundled here too, once, the first time a plan names one. Not thread-safe: use one
|
|
/// preview from one thread at a time.
|
|
/// </summary>
|
|
public sealed class RemoteNestPlanPreview
|
|
{
|
|
private readonly NestJob job;
|
|
private readonly IReadOnlyDictionary<string, Drawing> callerDrawings;
|
|
private readonly Dictionary<string, NestJobPart> parts;
|
|
private readonly Dictionary<string, Drawing> drawings = new(StringComparer.Ordinal);
|
|
private bool bundled;
|
|
|
|
public RemoteNestPlanPreview(NestPipelineRequest request, CancellationToken token = default)
|
|
{
|
|
(job, var byPart) = NestPipeline.CreateJob(request, token);
|
|
callerDrawings = byPart;
|
|
parts = job.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <summary>The plan as sheets, or null when <paramref name="planJson"/> is unreadable or
|
|
/// names parts this request does not have.</summary>
|
|
public IReadOnlyList<ProposedPlate> Sheets(string planJson, CancellationToken token = default)
|
|
{
|
|
if (RemoteNestPlan.FromJson(planJson)?.Plan is not { } plan)
|
|
return null;
|
|
if (!Known(plan) && !bundled)
|
|
{
|
|
bundled = true;
|
|
if (CutoutPipelinePrepass.Prepare(job, token)?.EngineJob is { } engineJob)
|
|
{
|
|
foreach (var part in engineJob.Parts)
|
|
parts.TryAdd(part.Id, part);
|
|
}
|
|
// A common-line trial's pair proxies, from either share (same geometry per part).
|
|
foreach (var share in new[] { CommonLinePipelinePrepass.Share.All, CommonLinePipelinePrepass.Share.Half })
|
|
if (CommonLinePipelinePrepass.Prepare(job, share, token)?.EngineJob is { } pairJob)
|
|
foreach (var part in pairJob.Parts)
|
|
parts.TryAdd(part.Id, part);
|
|
}
|
|
if (!Known(plan))
|
|
return null;
|
|
try
|
|
{
|
|
return NestPipeline.PlanSheets(plan.ToResult(job), parts, callerDrawings, drawings, token);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private bool Known(NestSolveTranscript.ResultJson plan) =>
|
|
plan.Plates.All(s => s?.Placements != null && s.Placements.All(p => p.PartId != null && parts.ContainsKey(p.PartId)));
|
|
}
|