mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 14:35: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>
162 lines
6.2 KiB
C#
162 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Engine.BestFit;
|
|
|
|
namespace OpenNest.Engine.Jobs.Adapters;
|
|
|
|
/// <summary>Explicit-ID input mapping and exact supported-geometry reconstruction. Never retains caller objects.</summary>
|
|
public static class DrawingJobMapper
|
|
{
|
|
public static NestJobPart FromDrawing(string partId, Drawing drawing, int quantity)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(drawing);
|
|
var constraints = drawing.Constraints;
|
|
return new NestJobPart(
|
|
partId,
|
|
PartGeometrySnapshot.FromProgram(drawing.Program),
|
|
quantity,
|
|
drawing.Priority,
|
|
constraints == null
|
|
? RotationPolicy.Automatic
|
|
: RotationPolicy.FromLegacy(
|
|
constraints.StepAngle,
|
|
constraints.StartAngle,
|
|
constraints.EndAngle,
|
|
constraints.Allow180Equivalent
|
|
)
|
|
)
|
|
{ AllowCommonLine = drawing.AllowCommonLine };
|
|
}
|
|
|
|
public static NestJobPart FromItem(string partId, NestItem item)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(item);
|
|
ArgumentNullException.ThrowIfNull(item.Drawing);
|
|
return new NestJobPart(
|
|
partId,
|
|
PartGeometrySnapshot.FromProgram(item.Drawing.Program),
|
|
item.Quantity,
|
|
item.Priority,
|
|
RotationPolicy.FromLegacy(
|
|
item.StepAngle,
|
|
item.RotationStart,
|
|
item.RotationEnd,
|
|
item.Drawing.Constraints?.Allow180Equivalent ?? false
|
|
)
|
|
)
|
|
{ AllowCommonLine = item.Drawing.AllowCommonLine };
|
|
}
|
|
|
|
/// <summary>Available stock is explicit; the legacy plate repeat count is not inventory.</summary>
|
|
public static NestPlateStock FromPlate(string stockId, Plate plate, int? quantity)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(plate);
|
|
return new NestPlateStock(
|
|
stockId,
|
|
plate.Size,
|
|
quantity,
|
|
plate.PartSpacing,
|
|
plate.EdgeSpacing,
|
|
plate.Quadrant,
|
|
cost: null,
|
|
clampZones: new List<ClampZone>(plate.ClampZones),
|
|
outline: plate.Outline
|
|
)
|
|
{ ClampRail = plate.ClampRail };
|
|
}
|
|
|
|
public static Program ToProgram(PartGeometrySnapshot geometry)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(geometry);
|
|
var program = new Program(geometry.Mode);
|
|
foreach (var motion in geometry.Motions)
|
|
{
|
|
var code = motion.Type switch
|
|
{
|
|
CodeType.RapidMove => (Motion)new RapidMove(motion.X, motion.Y),
|
|
CodeType.LinearMove => new LinearMove(motion.X, motion.Y) { Layer = motion.Layer },
|
|
CodeType.ArcMove => new ArcMove(
|
|
motion.X,
|
|
motion.Y,
|
|
motion.CenterX,
|
|
motion.CenterY,
|
|
motion.Rotation
|
|
)
|
|
{
|
|
Layer = motion.Layer,
|
|
},
|
|
_ => throw new NotSupportedException("Unsupported snapshot motion."),
|
|
};
|
|
code.Suppressed = motion.Suppressed;
|
|
program.Codes.Add(code);
|
|
}
|
|
return program;
|
|
}
|
|
|
|
/// <summary>A private drawing of the part for placement code. Its best-fit candidates are
|
|
/// pooled by geometry content (<see cref="BestFitCache.ShareByGeometry"/>), so the same part
|
|
/// geometry in a later job, engine instance or Auto Nest run does not search again.</summary>
|
|
internal static Drawing CreateDrawing(NestJobPart part)
|
|
{
|
|
var drawing = new Drawing(part.Id, ToProgram(part.Geometry))
|
|
{
|
|
Priority = part.Priority,
|
|
AllowCommonLine = part.AllowCommonLine,
|
|
};
|
|
drawing.Quantity.Required = part.Quantity;
|
|
drawing.Constraints = new NestConstraints
|
|
{
|
|
StepAngle = LegacyStep(part.Rotation),
|
|
StartAngle = part.Rotation.Start,
|
|
EndAngle = part.Rotation.End,
|
|
Allow180Equivalent = part.Rotation.Allow180Equivalent,
|
|
};
|
|
BestFitCache.ShareByGeometry(drawing, part.Geometry);
|
|
return drawing;
|
|
}
|
|
|
|
// A fixed angle needs a nonzero legacy step so it is not misread as automatic.
|
|
internal static double LegacyStep(RotationPolicy policy) =>
|
|
policy.Kind == RotationPolicyKind.Fixed ? OpenNest.Math.Angle.TwoPI : policy.Step;
|
|
|
|
internal static Plate CreatePlate(NestPlateStock stock)
|
|
{
|
|
var plate = new Plate(stock.Size)
|
|
{
|
|
Quantity = 1,
|
|
PartSpacing = stock.PartSpacing,
|
|
EdgeSpacing = stock.EdgeSpacing,
|
|
Quadrant = stock.Quadrant,
|
|
};
|
|
// Every Plate stores zones in plate (sheet lower-left) coordinates; consumers map
|
|
// them into the placement frame through ClampZoneFrame, exactly as the stock did.
|
|
foreach (var zone in stock.SheetClampZones)
|
|
plate.ClampZones.Add(new ClampZone(zone.Id, zone.Bounds));
|
|
plate.Outline = stock.Outline;
|
|
// The rail supplies the clamp clearance, so the plate is checked as the stock was packed.
|
|
if (stock.ClampRail != null)
|
|
FollowClampRail(plate, stock.ClampRail);
|
|
return plate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ties the plate to <paramref name="rail"/> when the rail recreates its zones, directly or
|
|
/// minus the clamps they lack (Auto Nest's last-resort removal, D8), so it keeps following
|
|
/// the rail and the removal (C13). Other zones stay explicit: tying them to the rail would
|
|
/// swap in other clamps.
|
|
/// </summary>
|
|
internal static void FollowClampRail(Plate plate, ClampRail rail)
|
|
{
|
|
var kept = new HashSet<string>(plate.ClampZones.Select(z => z.Id), StringComparer.Ordinal);
|
|
var removedClamps = rail.ResolveFor(plate.Size, plate.Quadrant)
|
|
.Select(z => z.Id)
|
|
.Where(id => !kept.Contains(id))
|
|
.ToList();
|
|
var unchanged = ReferenceEquals(plate.ClampRail, rail) && plate.RemovedClamps.SetEquals(removedClamps);
|
|
if (!unchanged && rail.Matches(plate, removedClamps))
|
|
plate.SetClampRail(rail, removedClamps);
|
|
}
|
|
}
|