fix(opus55): honor part priority and use host scoring and tolerances
Opus55 ignored NestJobPart.Priority, so the shared contract test (lower number wins scarce stock) failed; lower-number priority now precedes its placement score. SheetEconomics is replaced by the host's NestJobCost so it optimizes exactly what the benchmark scores, and its footprint margin comes from NestTolerances.SafeClearanceMargin plus four Clipper grid units - the same 0.003 total as before, which keeps its contact points. Synthetic benchmark (5 jobs, salvage 0.5): all valid, cost unchanged at 5452.79, time 611 -> 456 ms. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,25 +63,9 @@ internal sealed class FrontierPacker
|
|||||||
this.stock = stock;
|
this.stock = stock;
|
||||||
this.axis = axis;
|
this.axis = axis;
|
||||||
this.beta = beta;
|
this.beta = beta;
|
||||||
work = WorkArea(stock);
|
work = stock.WorkArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Box WorkArea(NestPlateStock stock)
|
|
||||||
{
|
|
||||||
var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
|
|
||||||
var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
|
|
||||||
return new Box(
|
|
||||||
left + stock.EdgeSpacing.Left,
|
|
||||||
bottom + stock.EdgeSpacing.Bottom,
|
|
||||||
stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right,
|
|
||||||
stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>True when the orientation's bounds fit the work area at all (Box.Length is the X extent).</summary>
|
|
||||||
public static bool Fits(Orientation o, Box work) =>
|
|
||||||
o.Width <= work.Length + 1e-9 && o.Height <= work.Width + 1e-9;
|
|
||||||
|
|
||||||
public SheetFill Fill(IReadOnlyList<int> remaining, CancellationToken token)
|
public SheetFill Fill(IReadOnlyList<int> remaining, CancellationToken token)
|
||||||
{
|
{
|
||||||
var left = remaining.ToArray();
|
var left = remaining.ToArray();
|
||||||
@@ -91,7 +75,7 @@ internal sealed class FrontierPacker
|
|||||||
if (left[type.Index] <= 0)
|
if (left[type.Index] <= 0)
|
||||||
continue;
|
continue;
|
||||||
foreach (var o in type.Orientations)
|
foreach (var o in type.Orientations)
|
||||||
if (Fits(o, work))
|
if (stock.Fits(o.Width, o.Height))
|
||||||
states.Add(new Region(o, work));
|
states.Add(new Region(o, work));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,17 +124,21 @@ internal sealed class FrontierPacker
|
|||||||
var bestValue = double.PositiveInfinity;
|
var bestValue = double.PositiveInfinity;
|
||||||
var bestSide = double.PositiveInfinity;
|
var bestSide = double.PositiveInfinity;
|
||||||
var bestLead = double.PositiveInfinity;
|
var bestLead = double.PositiveInfinity;
|
||||||
|
var bestPriority = int.MaxValue;
|
||||||
|
|
||||||
foreach (var region in states)
|
foreach (var region in states)
|
||||||
{
|
{
|
||||||
if (!region.TryLowest(axis, front, out var point, out var advance, out var side, out var lead))
|
if (!region.TryLowest(axis, front, out var point, out var advance, out var side, out var lead))
|
||||||
continue;
|
continue;
|
||||||
var area = types[region.Orientation.TypeIndex].Area;
|
var area = types[region.Orientation.TypeIndex].Area;
|
||||||
|
var priority = types[region.Orientation.TypeIndex].Part.Priority;
|
||||||
|
if (priority > bestPriority) continue;
|
||||||
var fills = advance <= Tie;
|
var fills = advance <= Tie;
|
||||||
// Gap fill prefers bigger parts (negated area); advance prefers least advance per area.
|
// Gap fill prefers bigger parts (negated area); advance prefers least advance per area.
|
||||||
var value = fills ? -area : advance / System.Math.Pow(System.Math.Max(area, 1e-12), beta);
|
var value = fills ? -area : advance / System.Math.Pow(System.Math.Max(area, 1e-12), beta);
|
||||||
|
|
||||||
var better = bestRegion == null
|
var better = bestRegion == null
|
||||||
|
|| priority < bestPriority
|
||||||
|| (fills && !bestFills)
|
|| (fills && !bestFills)
|
||||||
|| (
|
|| (
|
||||||
fills == bestFills
|
fills == bestFills
|
||||||
@@ -165,6 +153,7 @@ internal sealed class FrontierPacker
|
|||||||
if (!better)
|
if (!better)
|
||||||
continue;
|
continue;
|
||||||
bestRegion = region;
|
bestRegion = region;
|
||||||
|
bestPriority = priority;
|
||||||
bestPoint = point;
|
bestPoint = point;
|
||||||
bestFills = fills;
|
bestFills = fills;
|
||||||
bestValue = value;
|
bestValue = value;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Opus55;
|
namespace OpenNest.Engine.Opus55;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ namespace OpenNest.Engine.Opus55;
|
|||||||
internal sealed class NoFitCache
|
internal sealed class NoFitCache
|
||||||
{
|
{
|
||||||
/// <summary>Clipper decimal precision; 1e-4 job units is far below any margin we keep.</summary>
|
/// <summary>Clipper decimal precision; 1e-4 job units is far below any margin we keep.</summary>
|
||||||
public const int Precision = 4;
|
public const int Precision = NestTolerances.ClipperPrecision;
|
||||||
|
|
||||||
private readonly double halfClearance;
|
private readonly double halfClearance;
|
||||||
private readonly ConcurrentDictionary<(int, int), PathD> footprints = new();
|
private readonly ConcurrentDictionary<(int, int), PathD> footprints = new();
|
||||||
@@ -43,7 +44,10 @@ internal sealed class NoFitCache
|
|||||||
// footprint is a superset of "every point within the clearance of the outline".
|
// footprint is a superset of "every point within the clearance of the outline".
|
||||||
var inflated = Clipper.InflatePaths(
|
var inflated = Clipper.InflatePaths(
|
||||||
new PathsD { o.Outline },
|
new PathsD { o.Outline },
|
||||||
halfClearance + o.Tolerance,
|
// Four additional grid units cover this engine's repeated footprint/NFP
|
||||||
|
// Boolean operations. Keep its established contact points and packing quality.
|
||||||
|
halfClearance + NestTolerances.SafeClearanceMargin(o.Tolerance) / 2
|
||||||
|
+ 4 * System.Math.Pow(10, -Precision),
|
||||||
JoinType.Miter,
|
JoinType.Miter,
|
||||||
EndType.Polygon,
|
EndType.Polygon,
|
||||||
2.0,
|
2.0,
|
||||||
|
|||||||
@@ -19,17 +19,6 @@ namespace OpenNest.Engine.Opus55;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Opus55NestingEngine : INestingEngine
|
public sealed class Opus55NestingEngine : INestingEngine
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Extra clearance beyond the stock's part spacing, in job units. Footprints already contain
|
|
||||||
/// the true outline grown by half the clearance (chord tolerance is added separately), so this
|
|
||||||
/// only has to cover the validator's side: NestValidator flattens each perimeter within
|
|
||||||
/// 0.001 of the true arc (OutlineTolerance), so two true arcs can read up to 0.002 closer
|
|
||||||
/// than they are. The remaining 0.001 absorbs the 1e-4 Clipper grid on both sides (footprint
|
|
||||||
/// offset, Minkowski union, free-region difference, the validator's own offset); the
|
|
||||||
/// validator's round-join chord error (0.0001) only ever makes it more lenient.
|
|
||||||
/// </summary>
|
|
||||||
internal const double ClearanceMargin = 0.003;
|
|
||||||
|
|
||||||
/// <summary>Strategy variants, tried in order: (front direction, area exponent beta).</summary>
|
/// <summary>Strategy variants, tried in order: (front direction, area exponent beta).</summary>
|
||||||
private static readonly (PackAxis Axis, double Beta)[] Variants =
|
private static readonly (PackAxis Axis, double Beta)[] Variants =
|
||||||
{
|
{
|
||||||
@@ -54,6 +43,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(job);
|
ArgumentNullException.ThrowIfNull(job);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
var types = PartCatalog.Build(job);
|
var types = PartCatalog.Build(job);
|
||||||
var solver = new Solver(job, types, progress, token);
|
var solver = new Solver(job, types, progress, token);
|
||||||
|
|
||||||
@@ -63,7 +53,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
{
|
{
|
||||||
var placeable = job.Plates.Any(stock =>
|
var placeable = job.Plates.Any(stock =>
|
||||||
stock.Quantity != 0
|
stock.Quantity != 0
|
||||||
&& type.Orientations.Any(o => FrontierPacker.Fits(o, FrontierPacker.WorkArea(stock)))
|
&& type.Orientations.Any(o => stock.Fits(o.Width, o.Height))
|
||||||
);
|
);
|
||||||
demand[type.Index] = placeable ? type.Part.Quantity : 0;
|
demand[type.Index] = placeable ? type.Part.Quantity : 0;
|
||||||
}
|
}
|
||||||
@@ -99,7 +89,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
|
|
||||||
public WorkCounter Work { get; } = new();
|
public WorkCounter Work { get; } = new();
|
||||||
|
|
||||||
private double Penalty => job.Plates.Count == 0 ? 0 : job.Plates.Max(SheetEconomics.SheetArea);
|
private double Penalty => NestJobCost.UnplacedPartPenalty(job);
|
||||||
|
|
||||||
public Plan Plan(int[] demand, PackAxis axis, double beta)
|
public Plan Plan(int[] demand, PackAxis axis, double beta)
|
||||||
{
|
{
|
||||||
@@ -126,7 +116,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
var prefix = sheets.Take(sheets.Count - k).ToList();
|
var prefix = sheets.Take(sheets.Count - k).ToList();
|
||||||
var tail = sheets.Skip(sheets.Count - k).ToList();
|
var tail = sheets.Skip(sheets.Count - k).ToList();
|
||||||
var tailParts = tail.Sum(s => s.Parts.Count);
|
var tailParts = tail.Sum(s => s.Parts.Count);
|
||||||
var tailNet = tail.Sum(s => SheetEconomics.NetArea(job.Options, s));
|
var tailNet = tail.Sum(s => NetArea(job.Options, s));
|
||||||
var tailDemand = new int[types.Count];
|
var tailDemand = new int[types.Count];
|
||||||
foreach (var part in tail.SelectMany(s => s.Parts))
|
foreach (var part in tail.SelectMany(s => s.Parts))
|
||||||
tailDemand[part.Orientation.TypeIndex]++;
|
tailDemand[part.Orientation.TypeIndex]++;
|
||||||
@@ -158,7 +148,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
|
|
||||||
private NoFitCache CacheFor(NestPlateStock stock)
|
private NoFitCache CacheFor(NestPlateStock stock)
|
||||||
{
|
{
|
||||||
var clearance = System.Math.Max(0, stock.PartSpacing) + ClearanceMargin;
|
var clearance = System.Math.Max(0, stock.PartSpacing);
|
||||||
if (!caches.TryGetValue(clearance, out var cache))
|
if (!caches.TryGetValue(clearance, out var cache))
|
||||||
caches[clearance] = cache = new NoFitCache(clearance);
|
caches[clearance] = cache = new NoFitCache(clearance);
|
||||||
return cache;
|
return cache;
|
||||||
@@ -209,7 +199,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
var packer = new FrontierPacker(types, CacheFor(stock), stock, axis, beta, Work);
|
var packer = new FrontierPacker(types, CacheFor(stock), stock, axis, beta, Work);
|
||||||
var fill = packer.Fill(remaining, token);
|
var fill = packer.Fill(remaining, token);
|
||||||
if (fill.Parts.Count > 0)
|
if (fill.Parts.Count > 0)
|
||||||
trials.Add((fill, SheetEconomics.NetArea(job.Options, fill)));
|
trials.Add((fill, NetArea(job.Options, fill)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trials.Count == 0)
|
if (trials.Count == 0)
|
||||||
@@ -243,36 +233,23 @@ public sealed class Opus55NestingEngine : INestingEngine
|
|||||||
|
|
||||||
private sealed record Run(IReadOnlyList<SheetFill> Sheets, double Net, NestJobStopReason Reason);
|
private sealed record Run(IReadOnlyList<SheetFill> Sheets, double Net, NestJobStopReason Reason);
|
||||||
|
|
||||||
private static NestJobResult BuildResult(
|
private static NestJobResult BuildResult(NestJob job, IReadOnlyList<PartType> types,
|
||||||
NestJob job,
|
Plan plan, IProgress<NestJobProgress>? progress)
|
||||||
IReadOnlyList<PartType> types,
|
|
||||||
Plan plan,
|
|
||||||
IProgress<NestJobProgress>? progress
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var placed = new int[types.Count];
|
var builder = new NestJobResultBuilder(job, progress);
|
||||||
var plates = new List<NestJobPlateResult>(plan.Sheets.Count);
|
|
||||||
var committedParts = 0;
|
|
||||||
foreach (var sheet in plan.Sheets)
|
foreach (var sheet in plan.Sheets)
|
||||||
{
|
builder.AddSheet(sheet.Stock, sheet.Parts.Select(p =>
|
||||||
var placements = sheet.Parts.Select(p =>
|
(types[p.Orientation.TypeIndex].Part.Id, p.X, p.Y, p.Orientation.Rotation)));
|
||||||
{
|
return builder.Build(plan.Reason);
|
||||||
var type = types[p.Orientation.TypeIndex];
|
|
||||||
return new NestJobPlacement(type.Part.Id, placed[type.Index]++, p.X, p.Y, p.Orientation.Rotation);
|
|
||||||
});
|
|
||||||
plates.Add(new NestJobPlateResult(plates.Count, sheet.Stock, placements.ToList()));
|
|
||||||
committedParts += sheet.Parts.Count;
|
|
||||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, sheet.Stock.Id, plates.Count - 1, plates.Count, committedParts));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fulfillment = types.Select(t => new PartFulfillment(t.Part.Id, t.Part.Quantity, placed[t.Index], t.Part.Quantity - placed[t.Index]));
|
private static double NetArea(NestJobOptions options, SheetFill fill)
|
||||||
var usage = job.Plates.Select(stock =>
|
|
||||||
{
|
{
|
||||||
var count = plan.Sheets.Count(s => ReferenceEquals(s.Stock, stock));
|
if (fill.Parts.Count == 0) return fill.Stock.Area;
|
||||||
return new StockUsage(stock.Id, count, stock.Quantity - count);
|
var left = fill.Parts.Min(p => p.Left);
|
||||||
});
|
var bottom = fill.Parts.Min(p => p.Bottom);
|
||||||
var status = plan.Unplaced == 0 ? NestJobStatus.Complete : NestJobStatus.Incomplete;
|
return NestJobCost.NetSheetArea(options, fill.Stock, new OpenNest.Geometry.Box(left, bottom,
|
||||||
return new NestJobResult(status, plan.Reason, plates, fulfillment.ToList(), usage.ToList());
|
fill.Parts.Max(p => p.Right) - left, fill.Parts.Max(p => p.Top) - bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record Plan(IReadOnlyList<SheetFill> Sheets, double Cost, int Unplaced, NestJobStopReason Reason)
|
private sealed record Plan(IReadOnlyList<SheetFill> Sheets, double Cost, int Unplaced, NestJobStopReason Reason)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Opus55;
|
namespace OpenNest.Engine.Opus55;
|
||||||
@@ -56,8 +54,6 @@ internal static class PartCatalog
|
|||||||
/// <summary>Hard cap on distinct orientations evaluated per part type.</summary>
|
/// <summary>Hard cap on distinct orientations evaluated per part type.</summary>
|
||||||
private const int MaxOrientations = 8;
|
private const int MaxOrientations = 8;
|
||||||
|
|
||||||
private const double TwoPi = System.Math.PI * 2;
|
|
||||||
|
|
||||||
public static IReadOnlyList<PartType> Build(NestJob job)
|
public static IReadOnlyList<PartType> Build(NestJob job)
|
||||||
{
|
{
|
||||||
// Fewer orientations per type for jobs with many distinct parts; every (type, rotation)
|
// Fewer orientations per type for jobs with many distinct parts; every (type, rotation)
|
||||||
@@ -83,21 +79,15 @@ internal static class PartCatalog
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var angles = CandidateAngles(part.Rotation, perimeter, perType);
|
var angles = RotationCandidates.DistinctOutlines(perimeter,
|
||||||
|
CandidateAngles(part.Rotation, perimeter, perType));
|
||||||
var tolerance = ChooseTolerance(perimeter);
|
var tolerance = ChooseTolerance(perimeter);
|
||||||
var orientations = new List<Orientation>();
|
var orientations = new List<Orientation>();
|
||||||
var signatures = new List<string>();
|
|
||||||
foreach (var angle in angles)
|
foreach (var angle in angles)
|
||||||
{
|
{
|
||||||
var outline = Polygonize(perimeter, angle, tolerance);
|
var outline = Polygonize(perimeter, angle, tolerance);
|
||||||
if (outline.Count < 3)
|
if (outline.Count < 3)
|
||||||
continue;
|
continue;
|
||||||
// Point-symmetric parts (rectangles, discs...) look identical at several angles;
|
|
||||||
// evaluating duplicates only costs time.
|
|
||||||
var signature = Signature(outline);
|
|
||||||
if (signatures.Contains(signature))
|
|
||||||
continue;
|
|
||||||
signatures.Add(signature);
|
|
||||||
orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance));
|
orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,17 +97,8 @@ internal static class PartCatalog
|
|||||||
return types;
|
return types;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Shape? ReadPerimeter(PartGeometrySnapshot geometry)
|
private static Shape? ReadPerimeter(PartGeometrySnapshot geometry) =>
|
||||||
{
|
JobPartGeometry.TryRead(geometry)?.Perimeter;
|
||||||
var entities = ConvertProgram
|
|
||||||
.ToGeometry(DrawingJobMapper.ToProgram(geometry))
|
|
||||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
|
||||||
.ToList();
|
|
||||||
if (entities.Count == 0)
|
|
||||||
return null;
|
|
||||||
var profile = new ShapeProfile(entities);
|
|
||||||
return profile.Perimeter is { } perimeter && perimeter.Area() > 1e-9 ? perimeter : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Coarsens arc polygonization (up to 0.1% of the part size) until the outline is small
|
/// Coarsens arc polygonization (up to 0.1% of the part size) until the outline is small
|
||||||
@@ -170,105 +151,6 @@ internal static class PartCatalog
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Signature(PathD outline)
|
internal static List<double> CandidateAngles(RotationPolicy policy, Shape perimeter, int limit) =>
|
||||||
{
|
RotationCandidates.ForShape(policy, perimeter, limit).ToList();
|
||||||
var bounds = Clipper.GetBounds(outline);
|
|
||||||
var points = outline
|
|
||||||
.Select(p => (System.Math.Round(p.x - bounds.left, 5), System.Math.Round(p.y - bounds.top, 5)))
|
|
||||||
.OrderBy(p => p.Item1)
|
|
||||||
.ThenBy(p => p.Item2)
|
|
||||||
.Select(p => $"{p.Item1:R},{p.Item2:R}");
|
|
||||||
return string.Join(";", points);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rotations to try, all satisfying the part's policy. Automatic parts get the four
|
|
||||||
/// right angles plus the two orientations that align their minimum-area bounding
|
|
||||||
/// rectangle with the sheet axes.
|
|
||||||
/// </summary>
|
|
||||||
internal static List<double> CandidateAngles(RotationPolicy policy, Shape perimeter, int limit)
|
|
||||||
{
|
|
||||||
var raw = new List<double>();
|
|
||||||
switch (policy.Kind)
|
|
||||||
{
|
|
||||||
case RotationPolicyKind.Fixed:
|
|
||||||
raw.Add(policy.Start);
|
|
||||||
if (policy.Allow180Equivalent)
|
|
||||||
raw.Add(policy.Start + System.Math.PI);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RotationPolicyKind.BoundedSweep:
|
|
||||||
{
|
|
||||||
var steps = (int)System.Math.Floor((policy.End - policy.Start) / policy.Step + 1e-9);
|
|
||||||
var samples = System.Math.Min(steps + 1, policy.Allow180Equivalent ? System.Math.Max(1, limit / 2) : limit);
|
|
||||||
for (var i = 0; i < samples; i++)
|
|
||||||
{
|
|
||||||
var k = samples == 1 ? 0 : (int)System.Math.Round(i * (double)steps / (samples - 1));
|
|
||||||
raw.Add(policy.Start + k * policy.Step);
|
|
||||||
if (policy.Allow180Equivalent)
|
|
||||||
raw.Add(policy.Start + k * policy.Step + System.Math.PI);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
var rightAngles = new[] { 0, System.Math.PI / 2, System.Math.PI, System.Math.PI * 1.5 };
|
|
||||||
var aligned = AlignedAngle(perimeter);
|
|
||||||
raw.Add(0);
|
|
||||||
raw.Add(System.Math.PI / 2);
|
|
||||||
if (aligned is double a)
|
|
||||||
{
|
|
||||||
raw.Add(Normalize(a));
|
|
||||||
raw.Add(Normalize(a + System.Math.PI / 2));
|
|
||||||
}
|
|
||||||
raw.Add(System.Math.PI);
|
|
||||||
raw.Add(System.Math.PI * 1.5);
|
|
||||||
if (aligned is double b)
|
|
||||||
{
|
|
||||||
raw.Add(Normalize(b + System.Math.PI));
|
|
||||||
raw.Add(Normalize(b + System.Math.PI * 1.5));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new List<double>();
|
|
||||||
foreach (var angle in raw)
|
|
||||||
{
|
|
||||||
if (!policy.Allows(angle))
|
|
||||||
continue;
|
|
||||||
if (result.Any(existing => SameTurn(existing, angle)))
|
|
||||||
continue;
|
|
||||||
result.Add(angle);
|
|
||||||
if (result.Count >= limit)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double? AlignedAngle(Shape perimeter)
|
|
||||||
{
|
|
||||||
var polygon = perimeter.ToPolygonWithTolerance(ChordTolerance * 5);
|
|
||||||
if (polygon.Vertices.Count < 3)
|
|
||||||
return null;
|
|
||||||
var mbr = RotatingCalipers.MinimumBoundingRectangle(polygon.Vertices);
|
|
||||||
var angle = Normalize(-mbr.Angle) % (System.Math.PI / 2);
|
|
||||||
// Already axis-aligned (within ~0.05°): the right angles cover it.
|
|
||||||
if (angle < 1e-3 || System.Math.PI / 2 - angle < 1e-3)
|
|
||||||
return null;
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double Normalize(double angle)
|
|
||||||
{
|
|
||||||
var value = angle % TwoPi;
|
|
||||||
return value < 0 ? value + TwoPi : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SameTurn(double a, double b)
|
|
||||||
{
|
|
||||||
var delta = System.Math.Abs(Normalize(a - b));
|
|
||||||
return delta < 1e-9 || TwoPi - delta < 1e-9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ Every placement decision (which part, which rotation, where, on which sheet) com
|
|||||||
coarsened for arc-heavy parts until the outline is ≤ ~64 vertices, capped at 0.1% of part size).
|
coarsened for arc-heavy parts until the outline is ≤ ~64 vertices, capped at 0.1% of part size).
|
||||||
- Candidate rotations come from the part's `RotationPolicy`: for `Automatic`, the four right
|
- Candidate rotations come from the part's `RotationPolicy`: for `Automatic`, the four right
|
||||||
angles plus the two orientations that axis-align the minimum-area bounding rectangle
|
angles plus the two orientations that axis-align the minimum-area bounding rectangle
|
||||||
(`RotatingCalipers`); for sweeps, up to 8 evenly spaced legal steps. Point-symmetric duplicates are dropped.
|
(`RotatingCalipers`); for sweeps, the host policy grid truncated by the engine's orientation limit. Point-symmetric duplicates are dropped.
|
||||||
- Each orientation gets a **footprint**: outline inflated (miter joins, so it contains the exact
|
- Each orientation gets a **footprint**: outline inflated (miter joins, so it contains the exact
|
||||||
round offset) by `(spacing + 0.003) / 2 + chordTolerance`. Two parts respect the spacing
|
round offset) by `spacing / 2 + NestTolerances.SafeClearanceMargin(chordTolerance) / 2`
|
||||||
when their footprints don't overlap. The 0.003 covers `NestValidator` flattening each arc
|
plus four Clipper grid units per footprint. The extra grid allowance preserves this
|
||||||
within 0.001 of true (0.002 for a pair), plus Clipper's 1e-4 grid on both sides.
|
engine's established contact points through repeated NFP Boolean operations; removing it
|
||||||
|
increased mixed-job cost from 1586.94 to 1589.81 in the migration check.
|
||||||
- **No-fit polygons** between footprints come from Clipper2 Minkowski sums: an O(n+m)
|
- **No-fit polygons** between footprints come from Clipper2 Minkowski sums: an O(n+m)
|
||||||
edge merge for convex pairs, and for concave pairs the boundary sweep ∪ (A + p₀) ∪ (−B + a₀).
|
edge merge for convex pairs, and for concave pairs the boundary sweep ∪ (A + p₀) ∪ (−B + a₀).
|
||||||
The last two terms cover "B inside A" and "B swallows A". NFPs are cached per orientation pair.
|
The last two terms cover "B inside A" and "B swallows A". NFPs are cached per orientation pair.
|
||||||
@@ -27,14 +28,14 @@ Every placement decision (which part, which rotation, where, on which sheet) com
|
|||||||
legal reference points: the inner-fit rectangle minus the NFPs of everything placed. Each
|
legal reference points: the inner-fit rectangle minus the NFPs of everything placed. Each
|
||||||
placement subtracts one translated NFP from each region (in parallel, which stays deterministic).
|
placement subtracts one translated NFP from each region (in parallel, which stays deterministic).
|
||||||
Regions only shrink, and an empty region is retired for the rest of the sheet.
|
Regions only shrink, and an empty region is retired for the rest of the sheet.
|
||||||
- At every step all remaining types × orientations compete (there is no fixed placement sequence):
|
- At every step the lowest-number priority with a feasible placement wins; peer types × orientations compete (there is no fixed placement sequence):
|
||||||
1. **Gap fill:** if any part fits without pushing the packing front forward, place the
|
1. **Gap fill:** if any part fits without pushing the packing front forward, place the
|
||||||
*largest* such part at its lowest point.
|
*largest* such part at its lowest point.
|
||||||
2. **Advance:** otherwise place the part with the least front advance per `area^β`, i.e. the
|
2. **Advance:** otherwise place the part with the least front advance per `area^β`, i.e. the
|
||||||
most material coverage for the sheet length it consumes.
|
most material coverage for the sheet length it consumes.
|
||||||
- The front sweeps along X or Y, which leaves one full-width offcut strip for salvage credit.
|
- The front sweeps along X or Y, which leaves one full-width offcut strip for salvage credit.
|
||||||
|
|
||||||
**3. Whole job (`Opus55NestingEngine`, `SheetEconomics`)**
|
**3. Whole job (`Opus55NestingEngine`, `NestJobCost`)**
|
||||||
- Sheet by sheet, every available stock size is trial-filled. The trial with the lowest
|
- Sheet by sheet, every available stock size is trial-filled. The trial with the lowest
|
||||||
*estimated whole-job cost* (its net area, plus the remaining demand priced at the best
|
*estimated whole-job cost* (its net area, plus the remaining demand priced at the best
|
||||||
efficiency any trial achieved) is committed. This lets a sheet that finishes the job beat a
|
efficiency any trial achieved) is committed. This lets a sheet that finishes the job beat a
|
||||||
@@ -55,8 +56,7 @@ Every placement decision (which part, which rotation, where, on which sheet) com
|
|||||||
| `FrontierPacker.cs` | One-sheet fill: free regions and the gap-fill/advance choice rule |
|
| `FrontierPacker.cs` | One-sheet fill: free regions and the gap-fill/advance choice rule |
|
||||||
| `NoFitCache.cs` | Spacing footprints and cached NFPs (Clipper2 Minkowski) |
|
| `NoFitCache.cs` | Spacing footprints and cached NFPs (Clipper2 Minkowski) |
|
||||||
| `PartCatalog.cs` | Snapshot → perimeter polygon per allowed orientation |
|
| `PartCatalog.cs` | Snapshot → perimeter polygon per allowed orientation |
|
||||||
| `SheetEconomics.cs` | Net-area objective with salvage credit |
|
| `tests/` | xUnit suite. Layouts are judged by `Engine.Testing.LayoutAssert` and `NestLayoutCheck` |
|
||||||
| `tests/` | xUnit suite. Layouts are judged by `OpenNest.Benchmark.NestValidator` |
|
|
||||||
|
|
||||||
## Build / test
|
## Build / test
|
||||||
|
|
||||||
@@ -91,7 +91,17 @@ The engine reports as `Opus55NestingEngine`.
|
|||||||
distinct parts: `48 / partCount`, minimum 2). Free-angle rotations aren't explored beyond the MBR alignment.
|
distinct parts: `48 / partCount`, minimum 2). Free-angle rotations aren't explored beyond the MBR alignment.
|
||||||
- **Greedy core:** there is no order/permutation search. The variants and tail re-plan are the only
|
- **Greedy core:** there is no order/permutation search. The variants and tail re-plan are the only
|
||||||
search, and density on small mixed jobs trails what an interlocking-pair filler can reach.
|
search, and density on small mixed jobs trails what an interlocking-pair filler can reach.
|
||||||
- **`NestJobPart.Priority` is ignored**, and progress reports only `EvaluatingCandidate`
|
- **Priority is enforced during placement** (lower number first). Progress reports
|
||||||
per trial and `PlateCommitted` at the end, with no finer-grained progress.
|
`EvaluatingCandidate` per trial and `PlateCommitted` at the end, with no finer-grained progress.
|
||||||
- Parts whose geometry has no readable closed perimeter, or that fit no offered stock at any
|
- Parts whose geometry has no readable closed perimeter, or that fit no offered stock at any
|
||||||
allowed rotation, are reported unplaced (`NoPlacementFound`) instead of failing the job.
|
allowed rotation, are reported unplaced (`NoPlacementFound`) instead of failing the job.
|
||||||
|
|
||||||
|
## Shared services migration
|
||||||
|
|
||||||
|
`JobPartGeometry.TryRead` supplies the perimeter. `ForShape(policy, perimeter, limit)`
|
||||||
|
retains the engine's orientation cap and `DistinctOutlines` drops perimeter symmetry.
|
||||||
|
Stock `WorkArea`/`Fits`, host salvage scoring and `NestJobResultBuilder` replace copied
|
||||||
|
plumbing. The frontier, NFP cache, variant work budget and tail improvement remain local.
|
||||||
|
The shared contract suite exposed and now guards lower-number priority precedence.
|
||||||
|
Every old engine-specific test remains. All five salvage benchmark costs and validity
|
||||||
|
match baseline; see [PR 5 results](../MIGRATION-PR5.md).
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
using OpenNest.Engine.Jobs;
|
|
||||||
|
|
||||||
namespace OpenNest.Engine.Opus55;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The objective the engine optimizes: sheet area consumed, less the salvage credit for the
|
|
||||||
/// single largest full-width or full-length edge offcut the job's options allow. Packing toward
|
|
||||||
/// one edge (see <see cref="PackAxis"/>) is what makes that offcut large.
|
|
||||||
/// </summary>
|
|
||||||
internal static class SheetEconomics
|
|
||||||
{
|
|
||||||
public static double SheetArea(NestPlateStock stock) => stock.Size.Width * stock.Size.Length;
|
|
||||||
|
|
||||||
public static double NetArea(NestJobOptions options, SheetFill fill)
|
|
||||||
{
|
|
||||||
var area = SheetArea(fill.Stock);
|
|
||||||
var minimum = options.MinimumSalvageDimension;
|
|
||||||
if (options.SalvageRate <= 0 || minimum <= 0 || fill.Parts.Count == 0)
|
|
||||||
return area;
|
|
||||||
|
|
||||||
var work = FrontierPacker.WorkArea(fill.Stock);
|
|
||||||
var gap = fill.Stock.PartSpacing;
|
|
||||||
var left = fill.Parts.Min(p => p.Left);
|
|
||||||
var right = fill.Parts.Max(p => p.Right);
|
|
||||||
var bottom = fill.Parts.Min(p => p.Bottom);
|
|
||||||
var top = fill.Parts.Max(p => p.Top);
|
|
||||||
var offcuts = new[]
|
|
||||||
{
|
|
||||||
// Box.Length is the X extent, Box.Width the Y extent.
|
|
||||||
(work.Length, bottom - work.Bottom - gap),
|
|
||||||
(work.Length, work.Top - top - gap),
|
|
||||||
(left - work.Left - gap, work.Width),
|
|
||||||
(work.Right - right - gap, work.Width),
|
|
||||||
};
|
|
||||||
var salvage = 0.0;
|
|
||||||
foreach (var (a, b) in offcuts)
|
|
||||||
if (a >= minimum && b >= minimum)
|
|
||||||
salvage = System.Math.Max(salvage, a * b);
|
|
||||||
return area - options.SalvageRate * salvage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="../../Engine.Testing/OpenNest.Engine.Testing.csproj" />
|
||||||
<ProjectReference Include="../OpenNest.Engine.Opus55.csproj" />
|
<ProjectReference Include="../OpenNest.Engine.Opus55.csproj" />
|
||||||
<!-- The benchmark's NestValidator is the arbiter the engine is scored by. -->
|
|
||||||
<ProjectReference Include="$(OpenNestRoot)OpenNest.Benchmark/OpenNest.Benchmark.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using OpenNest.Engine.Testing;
|
||||||
|
using static OpenNest.Engine.Testing.JobBuilder;
|
||||||
|
using static OpenNest.Engine.Testing.Shapes;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenNest.Benchmark;
|
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
@@ -18,7 +20,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Assert.Equal(12, result.Plates[0].Placements.Count);
|
Assert.Equal(12, result.Plates[0].Placements.Count);
|
||||||
@@ -44,7 +46,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.True(result.Plates.Count > 1);
|
Assert.True(result.Plates.Count > 1);
|
||||||
Assert.Equal(25, result.Plates.Sum(p => p.Placements.Count));
|
Assert.Equal(25, result.Plates.Sum(p => p.Placements.Count));
|
||||||
@@ -120,7 +122,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
foreach (var placement in result.Plates.SelectMany(p => p.Placements))
|
foreach (var placement in result.Plates.SelectMany(p => p.Placements))
|
||||||
{
|
{
|
||||||
var policy = placement.PartId == "fixed" ? fixedPolicy : sweep;
|
var policy = placement.PartId == "fixed" ? fixedPolicy : sweep;
|
||||||
@@ -138,7 +140,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
||||||
Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason);
|
Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason);
|
||||||
Assert.Equal(1, result.Fulfillment.Single(f => f.PartId == "huge").Unplaced);
|
Assert.Equal(1, result.Fulfillment.Single(f => f.PartId == "huge").Unplaced);
|
||||||
@@ -152,7 +154,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStopReason.StockExhausted, result.StopReason);
|
Assert.Equal(NestJobStopReason.StockExhausted, result.StopReason);
|
||||||
Assert.Equal(2, result.Plates.Count);
|
Assert.Equal(2, result.Plates.Count);
|
||||||
var usage = Assert.Single(result.StockUsage);
|
var usage = Assert.Single(result.StockUsage);
|
||||||
@@ -171,7 +173,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
|
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ public class Opus55NestingEngineTests
|
|||||||
var first = new Opus55NestingEngine().Solve(Build());
|
var first = new Opus55NestingEngine().Solve(Build());
|
||||||
var second = new Opus55NestingEngine().Solve(Build());
|
var second = new Opus55NestingEngine().Solve(Build());
|
||||||
|
|
||||||
Assert.Equal(Describe(first), Describe(second));
|
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(first), System.Text.Json.JsonSerializer.Serialize(second));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -204,7 +206,7 @@ public class Opus55NestingEngineTests
|
|||||||
|
|
||||||
var result = new Opus55NestingEngine().Solve(job);
|
var result = new Opus55NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||||
}
|
}
|
||||||
@@ -216,86 +218,6 @@ public class Opus55NestingEngineTests
|
|||||||
Assert.IsAssignableFrom<INestingEngine>(engine);
|
Assert.IsAssignableFrom<INestingEngine>(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- helpers -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static string Describe(NestJobResult result) =>
|
|
||||||
string.Join(
|
|
||||||
"|",
|
|
||||||
result.Plates.Select(p =>
|
|
||||||
p.StockId + ":" + string.Join(",", p.Placements.Select(x => $"{x.PartId}#{x.InstanceIndex}@{x.X:R},{x.Y:R},{x.Rotation:R}"))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private static void AssertValid(NestJob job, NestJobResult result)
|
|
||||||
{
|
|
||||||
var materialized = NestResultMaterializer.Materialize(job, result);
|
|
||||||
var runs = materialized.Nest.Plates.Select(plate => (Plate: plate, Parts: plate.Parts.ToList())).ToList();
|
|
||||||
var requirements = job.Parts.ToDictionary<NestJobPart, Drawing, (string Name, int Quantity)>(
|
|
||||||
p => materialized.DrawingsByPartId[p.Id],
|
|
||||||
p => (p.Id, p.Quantity),
|
|
||||||
ReferenceEqualityComparer.Instance
|
|
||||||
);
|
|
||||||
var validation = NestValidator.Validate(runs, requirements);
|
|
||||||
NestValidator.ValidateAgainstJob(job, result, job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
|
||||||
Assert.True(validation.Valid, string.Join(Environment.NewLine, validation.Violations));
|
|
||||||
|
|
||||||
foreach (var f in result.Fulfillment)
|
|
||||||
Assert.Equal(f.Requested, f.Placed + f.Unplaced);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static NestJob Job(NestJobPart[] parts, NestPlateStock[] stock, NestJobOptions? options = null) =>
|
|
||||||
new(parts, stock, options);
|
|
||||||
|
|
||||||
private static NestJobPart Part(string id, Program program, int quantity, RotationPolicy? rotation = null) =>
|
|
||||||
new(id, PartGeometrySnapshot.FromProgram(program), quantity, 0, rotation);
|
|
||||||
|
|
||||||
/// <param name="width">Y extent.</param>
|
|
||||||
/// <param name="length">X extent.</param>
|
|
||||||
private static NestPlateStock Stock(
|
|
||||||
string id,
|
|
||||||
double width,
|
|
||||||
double length,
|
|
||||||
double spacing = 0,
|
|
||||||
Spacing edge = default,
|
|
||||||
int quadrant = 1,
|
|
||||||
int? quantity = null
|
|
||||||
) => new(id, new Size(width, length), quantity, spacing, edge, quadrant);
|
|
||||||
|
|
||||||
private static Program Polyline(params (double X, double Y)[] points)
|
|
||||||
{
|
|
||||||
var program = new Program();
|
|
||||||
program.Codes.Add(new RapidMove(points[0].X, points[0].Y));
|
|
||||||
foreach (var (x, y) in points.Skip(1))
|
|
||||||
program.Codes.Add(new LinearMove(x, y));
|
|
||||||
program.Codes.Add(new LinearMove(points[0].X, points[0].Y));
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Program Rectangle(double w, double h) => Polyline((0, 0), (w, 0), (w, h), (0, h));
|
|
||||||
|
|
||||||
private static Program Triangle(double w, double h) => Polyline((0, 0), (w, 0), (w * 0.3, h));
|
|
||||||
|
|
||||||
private static Program LShape(double w, double h, double t) => Polyline((0, 0), (w, 0), (w, t), (t, t), (t, h), (0, h));
|
|
||||||
|
|
||||||
private static Program Disc(double r)
|
|
||||||
{
|
|
||||||
var program = new Program();
|
|
||||||
program.Codes.Add(new RapidMove(r, 0));
|
|
||||||
program.Codes.Add(new ArcMove(-r, 0, 0, 0, RotationType.CCW));
|
|
||||||
program.Codes.Add(new ArcMove(r, 0, 0, 0, RotationType.CCW));
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Stadium: two semicircular ends joined by straight sides, offset from the origin.</summary>
|
|
||||||
private static Program Obround(double length, double width)
|
|
||||||
{
|
|
||||||
var r = width / 2;
|
|
||||||
var program = new Program();
|
|
||||||
program.Codes.Add(new RapidMove(1 + r, 1));
|
|
||||||
program.Codes.Add(new LinearMove(1 + length - r, 1));
|
|
||||||
program.Codes.Add(new ArcMove(1 + length - r, 1 + width, 1 + length - r, 1 + r, RotationType.CCW));
|
|
||||||
program.Codes.Add(new LinearMove(1 + r, 1 + width));
|
|
||||||
program.Codes.Add(new ArcMove(1 + r, 1, 1 + r, 1 + r, RotationType.CCW));
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class Opus55ContractTests : EngineContractTests<Opus55NestingEngine> { }
|
||||||
|
|||||||
Reference in New Issue
Block a user