style: apply CSharpier formatting to all C# sources
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests pass after reformat, full solution builds 0 errors. Added .csharpierignore so csproj/config XML keeps its existing layout (CSharpier's XML wrapping churns attributes with zero benefit). Formatting is now enforceable: dotnet csharpier check . passes.
This commit is contained in:
@@ -10,24 +10,46 @@ public static class DrawingJobMapper
|
||||
{
|
||||
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));
|
||||
return new NestJobPart(
|
||||
partId,
|
||||
PartGeometrySnapshot.FromProgram(drawing.Program),
|
||||
quantity,
|
||||
drawing.Priority,
|
||||
constraints == null
|
||||
? RotationPolicy.Automatic
|
||||
: RotationPolicy.FromLegacy(
|
||||
constraints.StepAngle,
|
||||
constraints.StartAngle,
|
||||
constraints.EndAngle
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
return new NestJobPart(
|
||||
partId,
|
||||
PartGeometrySnapshot.FromProgram(item.Drawing.Program),
|
||||
item.Quantity,
|
||||
item.Priority,
|
||||
RotationPolicy.FromLegacy(item.StepAngle, item.RotationStart, item.RotationEnd)
|
||||
);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
return new NestPlateStock(
|
||||
stockId,
|
||||
plate.Size,
|
||||
quantity,
|
||||
plate.PartSpacing,
|
||||
plate.EdgeSpacing,
|
||||
plate.Quadrant
|
||||
);
|
||||
}
|
||||
|
||||
public static Program ToProgram(PartGeometrySnapshot geometry)
|
||||
@@ -40,9 +62,17 @@ public static class DrawingJobMapper
|
||||
{
|
||||
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.")
|
||||
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);
|
||||
@@ -58,20 +88,21 @@ public static class DrawingJobMapper
|
||||
{
|
||||
StepAngle = LegacyStep(part.Rotation),
|
||||
StartAngle = part.Rotation.Start,
|
||||
EndAngle = part.Rotation.End
|
||||
EndAngle = part.Rotation.End,
|
||||
};
|
||||
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 double LegacyStep(RotationPolicy policy) =>
|
||||
policy.Kind == RotationPolicyKind.Fixed ? OpenNest.Math.Angle.TwoPI : policy.Step;
|
||||
|
||||
internal static Plate CreatePlate(NestPlateStock stock) => new(stock.Size)
|
||||
{
|
||||
Quantity = 1,
|
||||
PartSpacing = stock.PartSpacing,
|
||||
EdgeSpacing = stock.EdgeSpacing,
|
||||
Quadrant = stock.Quadrant
|
||||
};
|
||||
internal static Plate CreatePlate(NestPlateStock stock) =>
|
||||
new(stock.Size)
|
||||
{
|
||||
Quantity = 1,
|
||||
PartSpacing = stock.PartSpacing,
|
||||
EdgeSpacing = stock.EdgeSpacing,
|
||||
Quadrant = stock.Quadrant,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,11 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester
|
||||
/// process-global NestEngineRegistry.</summary>
|
||||
public static IPlateNester Create(string strategy) => PlateNesterFactory.Create(strategy);
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public PlateCandidate Place(
|
||||
PlatePlacementRequest request,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -35,37 +38,50 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester
|
||||
{
|
||||
var drawing = DrawingJobMapper.CreateDrawing(requirement);
|
||||
identities.Add(drawing, requirement.Id);
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End
|
||||
});
|
||||
items.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End,
|
||||
}
|
||||
);
|
||||
}
|
||||
var engine = engineFactory(plate) ?? throw new InvalidOperationException("Legacy engine factory returned null.");
|
||||
var legacyProgress = progress == null ? null : new LegacyProgress(progress, request.Stock.Id);
|
||||
var engine =
|
||||
engineFactory(plate)
|
||||
?? throw new InvalidOperationException("Legacy engine factory returned null.");
|
||||
var legacyProgress =
|
||||
progress == null ? null : new LegacyProgress(progress, request.Stock.Id);
|
||||
var parts = engine.Nest(items, legacyProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (parts == null) throw new InvalidOperationException("Legacy engine returned null placements.");
|
||||
if (parts == null)
|
||||
throw new InvalidOperationException("Legacy engine returned null placements.");
|
||||
var placements = new List<NestJobPlacement>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part?.BaseDrawing == null || !identities.TryGetValue(part.BaseDrawing, out var id))
|
||||
throw new InvalidOperationException("Legacy placement does not reference a private requirement drawing.");
|
||||
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
throw new InvalidOperationException(
|
||||
"Legacy placement does not reference a private requirement drawing."
|
||||
);
|
||||
placements.Add(
|
||||
new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)
|
||||
);
|
||||
}
|
||||
return new PlateCandidate(placements);
|
||||
}
|
||||
|
||||
private sealed class LegacyProgress(IProgress<NestJobProgress> progress, string stockId) : IProgress<NestProgress>
|
||||
private sealed class LegacyProgress(IProgress<NestJobProgress> progress, string stockId)
|
||||
: IProgress<NestProgress>
|
||||
{
|
||||
public void Report(NestProgress value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value));
|
||||
progress.Report(
|
||||
new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,23 @@ public static class NestResultMaterializer
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
var nest = new Nest();
|
||||
var drawings = job.Parts.ToDictionary(p => p.Id, DrawingJobMapper.CreateDrawing, StringComparer.Ordinal);
|
||||
foreach (var drawing in drawings.Values) nest.Drawings.Add(drawing);
|
||||
var drawings = job.Parts.ToDictionary(
|
||||
p => p.Id,
|
||||
DrawingJobMapper.CreateDrawing,
|
||||
StringComparer.Ordinal
|
||||
);
|
||||
foreach (var drawing in drawings.Values)
|
||||
nest.Drawings.Add(drawing);
|
||||
foreach (var sheet in result.Plates)
|
||||
{
|
||||
var plate = DrawingJobMapper.CreatePlate(sheet.Stock);
|
||||
foreach (var pose in sheet.Placements)
|
||||
{
|
||||
if (!drawings.TryGetValue(pose.PartId, out var drawing))
|
||||
throw new ArgumentException("Result contains a requirement not present in the job.", nameof(result));
|
||||
throw new ArgumentException(
|
||||
"Result contains a requirement not present in the job.",
|
||||
nameof(result)
|
||||
);
|
||||
// Do not use CreateAtOrigin: it normalizes bounds and would change the snapshot frame.
|
||||
var part = new Part(drawing);
|
||||
part.Rotate(pose.Rotation);
|
||||
|
||||
@@ -9,18 +9,25 @@ namespace OpenNest;
|
||||
/// </summary>
|
||||
internal static class CandidateProgressBridge
|
||||
{
|
||||
internal static IProgress<NestProgress> Create(IProgress<NestJobProgress> progress, string stockId)
|
||||
internal static IProgress<NestProgress> Create(
|
||||
IProgress<NestJobProgress> progress,
|
||||
string stockId
|
||||
)
|
||||
{
|
||||
if (progress == null) return null;
|
||||
if (progress == null)
|
||||
return null;
|
||||
return new LegacyToJob(progress, stockId);
|
||||
}
|
||||
|
||||
private sealed class LegacyToJob(IProgress<NestJobProgress> progress, string stockId) : IProgress<NestProgress>
|
||||
private sealed class LegacyToJob(IProgress<NestJobProgress> progress, string stockId)
|
||||
: IProgress<NestProgress>
|
||||
{
|
||||
public void Report(NestProgress value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value));
|
||||
progress.Report(
|
||||
new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,18 @@ public sealed class FixedStrategyNestingEngine : INestingEngine
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default)
|
||||
public NestJobResult Solve(
|
||||
NestJob job,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
var forced = new NestJob(job.Parts, job.Plates, new NestJobOptions(strategy, job.Options.MaxPlates));
|
||||
var forced = new NestJob(
|
||||
job.Parts,
|
||||
job.Plates,
|
||||
new NestJobOptions(strategy, job.Options.MaxPlates)
|
||||
);
|
||||
return runner.Solve(forced, progress, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,9 @@ namespace OpenNest;
|
||||
/// <summary>Synchronous whole-job solver. Cancellation throws, rather than returning partial success.</summary>
|
||||
public interface INestingEngine
|
||||
{
|
||||
NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default);
|
||||
NestJobResult Solve(
|
||||
NestJob job,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace OpenNest;
|
||||
/// <summary>Places on one sheet only. Must not change stock, demand, or caller-owned domain objects.</summary>
|
||||
public interface IPlateNester
|
||||
{
|
||||
PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default);
|
||||
PlateCandidate Place(
|
||||
PlatePlacementRequest request,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,19 @@ namespace OpenNest;
|
||||
/// <summary>One material/unit system's requirements. Collections are copied; all nested values are immutable.</summary>
|
||||
public sealed class NestJob
|
||||
{
|
||||
public NestJob(IEnumerable<NestJobPart> parts, IEnumerable<NestPlateStock> plates, NestJobOptions options = null)
|
||||
public NestJob(
|
||||
IEnumerable<NestJobPart> parts,
|
||||
IEnumerable<NestPlateStock> plates,
|
||||
NestJobOptions options = null
|
||||
)
|
||||
{
|
||||
Parts = Own(parts);
|
||||
Plates = Own(plates);
|
||||
Options = options ?? new NestJobOptions();
|
||||
if (Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count ||
|
||||
Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count)
|
||||
if (
|
||||
Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count
|
||||
|| Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count
|
||||
)
|
||||
throw new ArgumentException("Part and stock IDs must each be unique.");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,34 +15,49 @@ public sealed class NestJobCandidateComparer
|
||||
}
|
||||
|
||||
/// <summary>Returns positive when the left trial is preferred.</summary>
|
||||
public int Compare(PlateCandidate left, NestPlateStock leftStock, int leftIndex,
|
||||
PlateCandidate right, NestPlateStock rightStock, int rightIndex)
|
||||
public int Compare(
|
||||
PlateCandidate left,
|
||||
NestPlateStock leftStock,
|
||||
int leftIndex,
|
||||
PlateCandidate right,
|
||||
NestPlateStock rightStock,
|
||||
int rightIndex
|
||||
)
|
||||
{
|
||||
var priorities = parts.Select(part => part.Priority).Distinct().OrderBy(priority => priority);
|
||||
var priorities = parts
|
||||
.Select(part => part.Priority)
|
||||
.Distinct()
|
||||
.OrderBy(priority => priority);
|
||||
foreach (var priority in priorities)
|
||||
{
|
||||
var leftCount = Count(left, priority);
|
||||
var rightCount = Count(right, priority);
|
||||
if (leftCount != rightCount) return leftCount.CompareTo(rightCount);
|
||||
if (leftCount != rightCount)
|
||||
return leftCount.CompareTo(rightCount);
|
||||
}
|
||||
|
||||
var area = Area(rightStock).CompareTo(Area(leftStock));
|
||||
if (area != 0) return area;
|
||||
if (area != 0)
|
||||
return area;
|
||||
|
||||
var envelope = Envelope(right).CompareTo(Envelope(left));
|
||||
if (envelope != 0) return envelope;
|
||||
if (envelope != 0)
|
||||
return envelope;
|
||||
|
||||
return rightIndex.CompareTo(leftIndex);
|
||||
}
|
||||
|
||||
private int Count(PlateCandidate candidate, int priority) => candidate.Placements.Count(placement =>
|
||||
parts.First(part => part.Id == placement.PartId).Priority == priority);
|
||||
private int Count(PlateCandidate candidate, int priority) =>
|
||||
candidate.Placements.Count(placement =>
|
||||
parts.First(part => part.Id == placement.PartId).Priority == priority
|
||||
);
|
||||
|
||||
private static double Area(NestPlateStock stock) => stock.Size.Width * stock.Size.Length;
|
||||
|
||||
private static double Envelope(PlateCandidate candidate)
|
||||
{
|
||||
if (candidate.Placements.Count == 0) return 0;
|
||||
if (candidate.Placements.Count == 0)
|
||||
return 0;
|
||||
var xs = candidate.Placements.Select(placement => placement.X);
|
||||
var ys = candidate.Placements.Select(placement => placement.Y);
|
||||
return (xs.Max() - xs.Min()) * (ys.Max() - ys.Min());
|
||||
|
||||
@@ -5,11 +5,16 @@ namespace OpenNest;
|
||||
/// <summary>Immutable per-job options; selection never changes the legacy global registry.</summary>
|
||||
public sealed class NestJobOptions
|
||||
{
|
||||
public NestJobOptions(string placementStrategy = "Default", int? maxPlates = null,
|
||||
double salvageRate = 0, double minimumSalvageDimension = 0)
|
||||
public NestJobOptions(
|
||||
string placementStrategy = "Default",
|
||||
int? maxPlates = null,
|
||||
double salvageRate = 0,
|
||||
double minimumSalvageDimension = 0
|
||||
)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy);
|
||||
if (maxPlates <= 0) throw new ArgumentOutOfRangeException(nameof(maxPlates));
|
||||
if (maxPlates <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPlates));
|
||||
if (!double.IsFinite(salvageRate) || salvageRate < 0 || salvageRate > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(salvageRate));
|
||||
if (!double.IsFinite(minimumSalvageDimension) || minimumSalvageDimension < 0)
|
||||
@@ -22,11 +27,13 @@ public sealed class NestJobOptions
|
||||
|
||||
/// <summary>Fraction of eligible edge-offcut area credited by StockLadder (0..1).</summary>
|
||||
public double SalvageRate { get; }
|
||||
|
||||
/// <summary>Both offcut dimensions must meet this caller-supplied minimum in job units.
|
||||
/// Zero disables credit; scraps and holes are never credited.</summary>
|
||||
public double MinimumSalvageDimension { get; }
|
||||
|
||||
public string PlacementStrategy { get; }
|
||||
|
||||
/// <summary>Maximum physical sheets to commit, or null for no explicit cap.</summary>
|
||||
public int? MaxPlates { get; }
|
||||
}
|
||||
|
||||
@@ -5,12 +5,18 @@ namespace OpenNest;
|
||||
/// <summary>An immutable requirement, independent of drawing names, UI state, and drawing quantity counters.</summary>
|
||||
public sealed class NestJobPart
|
||||
{
|
||||
public NestJobPart(string id, PartGeometrySnapshot geometry, int quantity, int priority = 0,
|
||||
RotationPolicy rotation = null)
|
||||
public NestJobPart(
|
||||
string id,
|
||||
PartGeometrySnapshot geometry,
|
||||
int quantity,
|
||||
int priority = 0,
|
||||
RotationPolicy rotation = null
|
||||
)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentNullException.ThrowIfNull(geometry);
|
||||
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Geometry = geometry;
|
||||
Quantity = quantity;
|
||||
@@ -20,6 +26,7 @@ public sealed class NestJobPart
|
||||
|
||||
public string Id { get; }
|
||||
public PartGeometrySnapshot Geometry { get; }
|
||||
|
||||
/// <summary>Positive number requested; never decremented by placement code.</summary>
|
||||
public int Quantity { get; }
|
||||
public int Priority { get; }
|
||||
|
||||
@@ -11,33 +11,54 @@ internal static class NestJobPlacementValidator
|
||||
{
|
||||
private const double Epsilon = 0.0000001;
|
||||
|
||||
internal static void ValidateCandidate(PlateCandidate candidate, NestPlateStock stock,
|
||||
IReadOnlyDictionary<string, int> remaining, IReadOnlyDictionary<string, NestJobPart> parts)
|
||||
internal static void ValidateCandidate(
|
||||
PlateCandidate candidate,
|
||||
NestPlateStock stock,
|
||||
IReadOnlyDictionary<string, int> remaining,
|
||||
IReadOnlyDictionary<string, NestJobPart> parts
|
||||
)
|
||||
{
|
||||
if (candidate == null) throw new InvalidOperationException("The plate nester returned a null candidate.");
|
||||
if (candidate == null)
|
||||
throw new InvalidOperationException("The plate nester returned a null candidate.");
|
||||
var counts = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var placed = new List<ShapeTopology>();
|
||||
foreach (var placement in candidate.Placements)
|
||||
{
|
||||
if (placement.PartId == null || !remaining.TryGetValue(placement.PartId, out var available) ||
|
||||
!parts.TryGetValue(placement.PartId, out var part))
|
||||
throw new InvalidOperationException("Candidate references an unknown requirement ID.");
|
||||
if (!double.IsFinite(placement.X) || !double.IsFinite(placement.Y) || !double.IsFinite(placement.Rotation))
|
||||
if (
|
||||
placement.PartId == null
|
||||
|| !remaining.TryGetValue(placement.PartId, out var available)
|
||||
|| !parts.TryGetValue(placement.PartId, out var part)
|
||||
)
|
||||
throw new InvalidOperationException(
|
||||
"Candidate references an unknown requirement ID."
|
||||
);
|
||||
if (
|
||||
!double.IsFinite(placement.X)
|
||||
|| !double.IsFinite(placement.Y)
|
||||
|| !double.IsFinite(placement.Rotation)
|
||||
)
|
||||
throw new InvalidOperationException("Candidate poses must be finite.");
|
||||
counts.TryGetValue(placement.PartId, out var count);
|
||||
if (count >= available) throw new InvalidOperationException("Candidate overproduces a requirement.");
|
||||
if (count >= available)
|
||||
throw new InvalidOperationException("Candidate overproduces a requirement.");
|
||||
if (!RotationIsAllowed(part.Rotation, placement.Rotation))
|
||||
throw new InvalidOperationException("Candidate rotation is not allowed for the requirement.");
|
||||
throw new InvalidOperationException(
|
||||
"Candidate rotation is not allowed for the requirement."
|
||||
);
|
||||
|
||||
var shape = Transform(CreateShape(part.Geometry), placement);
|
||||
if (!FitsWorkArea(shape, stock))
|
||||
throw new InvalidOperationException("Candidate placement falls outside the usable stock area.");
|
||||
throw new InvalidOperationException(
|
||||
"Candidate placement falls outside the usable stock area."
|
||||
);
|
||||
foreach (var other in placed)
|
||||
{
|
||||
if (Overlaps(shape, other))
|
||||
throw new InvalidOperationException("Candidate placements overlap.");
|
||||
if (stock.PartSpacing > 0 && Distance(shape, other) < stock.PartSpacing - Epsilon)
|
||||
throw new InvalidOperationException("Candidate placements violate required part spacing.");
|
||||
throw new InvalidOperationException(
|
||||
"Candidate placements violate required part spacing."
|
||||
);
|
||||
}
|
||||
|
||||
placed.Add(shape);
|
||||
@@ -52,10 +73,12 @@ internal static class NestJobPlacementValidator
|
||||
|
||||
private static bool RotationIsAllowed(RotationPolicy policy, double rotation)
|
||||
{
|
||||
if (policy.Kind == RotationPolicyKind.Automatic) return true;
|
||||
if (policy.Kind == RotationPolicyKind.Automatic)
|
||||
return true;
|
||||
if (policy.Kind == RotationPolicyKind.Fixed)
|
||||
return AnglesEqual(rotation, policy.Start);
|
||||
if (rotation < policy.Start - Epsilon || rotation > policy.End + Epsilon) return false;
|
||||
if (rotation < policy.Start - Epsilon || rotation > policy.End + Epsilon)
|
||||
return false;
|
||||
var steps = (rotation - policy.Start) / policy.Step;
|
||||
return System.Math.Abs(steps - System.Math.Round(steps)) <= Epsilon;
|
||||
}
|
||||
@@ -63,7 +86,8 @@ internal static class NestJobPlacementValidator
|
||||
private static bool AnglesEqual(double left, double right)
|
||||
{
|
||||
var delta = (left - right) % (System.Math.PI * 2);
|
||||
return System.Math.Abs(delta) <= Epsilon || System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Epsilon;
|
||||
return System.Math.Abs(delta) <= Epsilon
|
||||
|| System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Epsilon;
|
||||
}
|
||||
|
||||
private static ShapeTopology CreateShape(PartGeometrySnapshot geometry)
|
||||
@@ -75,7 +99,8 @@ internal static class NestJobPlacementValidator
|
||||
cutEntities.Add(entity);
|
||||
|
||||
var contours = ShapeBuilder.GetShapes(cutEntities);
|
||||
if (contours.Count == 0) throw new ArgumentException("Geometry must contain a closed contour.");
|
||||
if (contours.Count == 0)
|
||||
throw new ArgumentException("Geometry must contain a closed contour.");
|
||||
var closedEntities = new List<Entity>();
|
||||
var marks = new List<Shape>();
|
||||
foreach (var contour in contours)
|
||||
@@ -85,7 +110,8 @@ internal static class NestJobPlacementValidator
|
||||
ValidateContour(contour);
|
||||
closedEntities.AddRange(contour.Entities);
|
||||
}
|
||||
else marks.Add(contour);
|
||||
else
|
||||
marks.Add(contour);
|
||||
}
|
||||
if (closedEntities.Count == 0)
|
||||
throw new ArgumentException("Geometry must contain a closed outer contour.");
|
||||
@@ -126,25 +152,32 @@ internal static class NestJobPlacementValidator
|
||||
for (var index = 0; index < parameters.Count; index++)
|
||||
{
|
||||
Check(PointAt(parameters[index]));
|
||||
if (index > 0) Check(PointAt((parameters[index - 1] + parameters[index]) / 2));
|
||||
if (index > 0)
|
||||
Check(PointAt((parameters[index - 1] + parameters[index]) / 2));
|
||||
}
|
||||
|
||||
void AddParameter(Vector point)
|
||||
{
|
||||
if (!point.IsValid()) throw new ArgumentException("Indeterminate mark intersection.");
|
||||
if (!point.IsValid())
|
||||
throw new ArgumentException("Indeterminate mark intersection.");
|
||||
var value = entity is Line line
|
||||
? line.StartPoint.DistanceTo(point) / line.Length
|
||||
: Angle.NormalizeRad(((Arc)entity).IsReversed
|
||||
? ((Arc)entity).StartAngle - ((Arc)entity).Center.AngleTo(point)
|
||||
: ((Arc)entity).Center.AngleTo(point) - ((Arc)entity).StartAngle) / ((Arc)entity).SweepAngle();
|
||||
if (value >= 0 && value <= 1) parameters.Add(value);
|
||||
: Angle.NormalizeRad(
|
||||
((Arc)entity).IsReversed
|
||||
? ((Arc)entity).StartAngle - ((Arc)entity).Center.AngleTo(point)
|
||||
: ((Arc)entity).Center.AngleTo(point) - ((Arc)entity).StartAngle
|
||||
) / ((Arc)entity).SweepAngle();
|
||||
if (value >= 0 && value <= 1)
|
||||
parameters.Add(value);
|
||||
}
|
||||
Vector PointAt(double value)
|
||||
{
|
||||
if (entity is Line line) return line.StartPoint + (line.EndPoint - line.StartPoint) * value;
|
||||
if (entity is Line line)
|
||||
return line.StartPoint + (line.EndPoint - line.StartPoint) * value;
|
||||
var arc = (Arc)entity;
|
||||
var angle = arc.StartAngle + (arc.IsReversed ? -1 : 1) * arc.SweepAngle() * value;
|
||||
return arc.Center + new Vector(System.Math.Cos(angle), System.Math.Sin(angle)) * arc.Radius;
|
||||
return arc.Center
|
||||
+ new Vector(System.Math.Cos(angle), System.Math.Sin(angle)) * arc.Radius;
|
||||
}
|
||||
void Check(Vector point)
|
||||
{
|
||||
@@ -153,14 +186,20 @@ internal static class NestJobPlacementValidator
|
||||
// Exact analytic boundary contact is allowed; near-boundary uncertainty is not.
|
||||
var onBoundary = false;
|
||||
foreach (var edge in boundaries[index].Entities)
|
||||
if (edge.ClosestPointTo(point).DistanceTo(point) <= Epsilon) onBoundary = true;
|
||||
if (onBoundary) continue;
|
||||
if (edge.ClosestPointTo(point).DistanceTo(point) <= Epsilon)
|
||||
onBoundary = true;
|
||||
if (onBoundary)
|
||||
continue;
|
||||
foreach (var edge in polygons[index].ToLines())
|
||||
if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance)
|
||||
throw new ArgumentException("Internal mark is too close to a material boundary.");
|
||||
throw new ArgumentException(
|
||||
"Internal mark is too close to a material boundary."
|
||||
);
|
||||
var inside = StrictlyInside(polygons[index], point);
|
||||
if (index == 0 ? !inside : inside)
|
||||
throw new ArgumentException("Open geometry leaves the closed material region.");
|
||||
throw new ArgumentException(
|
||||
"Open geometry leaves the closed material region."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,19 +223,25 @@ internal static class NestJobPlacementValidator
|
||||
Line line => line.StartPoint,
|
||||
Arc arc => arc.StartPoint(),
|
||||
Circle circle => circle.Center.Offset(circle.Radius, 0),
|
||||
_ => throw new ArgumentException("Unsupported internal geometry.")
|
||||
_ => throw new ArgumentException("Unsupported internal geometry."),
|
||||
};
|
||||
if (!StrictlyInside(polygons[0], point))
|
||||
throw new ArgumentException("Open or disconnected geometry lies outside the closed perimeter.");
|
||||
throw new ArgumentException(
|
||||
"Open or disconnected geometry lies outside the closed perimeter."
|
||||
);
|
||||
for (var index = 0; index < boundaries.Count; index++)
|
||||
{
|
||||
if (index > 0 && polygons[index].ContainsPoint(point))
|
||||
throw new ArgumentException("Internal geometry lies in a cutout.");
|
||||
foreach (var edge in polygons[index].ToLines())
|
||||
if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance)
|
||||
throw new ArgumentException("Internal geometry is too close to a material boundary.");
|
||||
throw new ArgumentException(
|
||||
"Internal geometry is too close to a material boundary."
|
||||
);
|
||||
if (entity.Intersects(boundaries[index]))
|
||||
throw new ArgumentException("Internal geometry crosses or touches a material boundary.");
|
||||
throw new ArgumentException(
|
||||
"Internal geometry crosses or touches a material boundary."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,9 +277,11 @@ internal static class NestJobPlacementValidator
|
||||
private static bool FitsWorkArea(ShapeTopology shape, NestPlateStock stock)
|
||||
{
|
||||
var workArea = WorkArea(stock);
|
||||
if (!FitsWorkArea(shape.Perimeter, workArea)) return false;
|
||||
if (!FitsWorkArea(shape.Perimeter, workArea))
|
||||
return false;
|
||||
foreach (var cutout in shape.Cutouts)
|
||||
if (!FitsWorkArea(cutout, workArea)) return false;
|
||||
if (!FitsWorkArea(cutout, workArea))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -242,16 +289,21 @@ internal static class NestJobPlacementValidator
|
||||
{
|
||||
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,
|
||||
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);
|
||||
stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top
|
||||
);
|
||||
}
|
||||
|
||||
private static bool FitsWorkArea(Shape contour, Box workArea)
|
||||
{
|
||||
var bounds = contour.BoundingBox;
|
||||
return bounds.Left >= workArea.Left - Epsilon && bounds.Right <= workArea.Right + Epsilon &&
|
||||
bounds.Bottom >= workArea.Bottom - Epsilon && bounds.Top <= workArea.Top + Epsilon;
|
||||
return bounds.Left >= workArea.Left - Epsilon
|
||||
&& bounds.Right <= workArea.Right + Epsilon
|
||||
&& bounds.Bottom >= workArea.Bottom - Epsilon
|
||||
&& bounds.Top <= workArea.Top + Epsilon;
|
||||
}
|
||||
|
||||
private static bool Overlaps(ShapeTopology left, ShapeTopology right)
|
||||
@@ -265,7 +317,12 @@ internal static class NestJobPlacementValidator
|
||||
// Collision checks this by clipping triangulated polygons and rejecting zero-area
|
||||
// slivers, so it catches containment and small corner intersections that a witness
|
||||
// probe can miss, while contact stays legal; cutouts are subtracted from both sides.
|
||||
return Collision.HasOverlap(leftPoly, rightPoly, ToPolygons(left.Cutouts), ToPolygons(right.Cutouts));
|
||||
return Collision.HasOverlap(
|
||||
leftPoly,
|
||||
rightPoly,
|
||||
ToPolygons(left.Cutouts),
|
||||
ToPolygons(right.Cutouts)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,13 +331,15 @@ internal static class NestJobPlacementValidator
|
||||
private static bool StrictlyInside(Polygon polygon, Vector point)
|
||||
{
|
||||
var n = polygon.IsClosed() ? polygon.Vertices.Count - 1 : polygon.Vertices.Count;
|
||||
if (n < 3) return false;
|
||||
if (n < 3)
|
||||
return false;
|
||||
var winding = 0;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var p1 = polygon.Vertices[i];
|
||||
var p2 = polygon.Vertices[(i + 1) % n];
|
||||
if (OnSegment(p1, p2, point)) return false;
|
||||
if (OnSegment(p1, p2, point))
|
||||
return false;
|
||||
if (p1.Y <= point.Y)
|
||||
{
|
||||
if (p2.Y > point.Y && IsLeft(p1, p2, point) > 0)
|
||||
@@ -297,9 +356,12 @@ internal static class NestJobPlacementValidator
|
||||
private static bool OnSegment(Vector a, Vector b, Vector p)
|
||||
{
|
||||
var cross = (b.X - a.X) * (p.Y - a.Y) - (b.Y - a.Y) * (p.X - a.X);
|
||||
if (!cross.IsEqualTo(0.0)) return false;
|
||||
return System.Math.Min(a.X, b.X) - Epsilon <= p.X && p.X <= System.Math.Max(a.X, b.X) + Epsilon &&
|
||||
System.Math.Min(a.Y, b.Y) - Epsilon <= p.Y && p.Y <= System.Math.Max(a.Y, b.Y) + Epsilon;
|
||||
if (!cross.IsEqualTo(0.0))
|
||||
return false;
|
||||
return System.Math.Min(a.X, b.X) - Epsilon <= p.X
|
||||
&& p.X <= System.Math.Max(a.X, b.X) + Epsilon
|
||||
&& System.Math.Min(a.Y, b.Y) - Epsilon <= p.Y
|
||||
&& p.Y <= System.Math.Max(a.Y, b.Y) + Epsilon;
|
||||
}
|
||||
|
||||
private static double IsLeft(Vector p1, Vector p2, Vector p) =>
|
||||
@@ -309,8 +371,11 @@ internal static class NestJobPlacementValidator
|
||||
{
|
||||
var result = double.PositiveInfinity;
|
||||
foreach (var leftContour in AllContours(left))
|
||||
foreach (var rightContour in AllContours(right))
|
||||
result = System.Math.Min(result, BoundaryDistance(ToPolygon(leftContour), ToPolygon(rightContour)));
|
||||
foreach (var rightContour in AllContours(right))
|
||||
result = System.Math.Min(
|
||||
result,
|
||||
BoundaryDistance(ToPolygon(leftContour), ToPolygon(rightContour))
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -343,11 +408,24 @@ internal static class NestJobPlacementValidator
|
||||
{
|
||||
foreach (var rightLine in right.ToLines())
|
||||
{
|
||||
if (leftLine.Intersects(rightLine)) return 0;
|
||||
result = System.Math.Min(result, leftLine.ClosestPointTo(rightLine.StartPoint).DistanceTo(rightLine.StartPoint));
|
||||
result = System.Math.Min(result, leftLine.ClosestPointTo(rightLine.EndPoint).DistanceTo(rightLine.EndPoint));
|
||||
result = System.Math.Min(result, rightLine.ClosestPointTo(leftLine.StartPoint).DistanceTo(leftLine.StartPoint));
|
||||
result = System.Math.Min(result, rightLine.ClosestPointTo(leftLine.EndPoint).DistanceTo(leftLine.EndPoint));
|
||||
if (leftLine.Intersects(rightLine))
|
||||
return 0;
|
||||
result = System.Math.Min(
|
||||
result,
|
||||
leftLine.ClosestPointTo(rightLine.StartPoint).DistanceTo(rightLine.StartPoint)
|
||||
);
|
||||
result = System.Math.Min(
|
||||
result,
|
||||
leftLine.ClosestPointTo(rightLine.EndPoint).DistanceTo(rightLine.EndPoint)
|
||||
);
|
||||
result = System.Math.Min(
|
||||
result,
|
||||
rightLine.ClosestPointTo(leftLine.StartPoint).DistanceTo(leftLine.StartPoint)
|
||||
);
|
||||
result = System.Math.Min(
|
||||
result,
|
||||
rightLine.ClosestPointTo(leftLine.EndPoint).DistanceTo(leftLine.EndPoint)
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
namespace OpenNest;
|
||||
|
||||
public enum NestJobStage { EvaluatingCandidate, PlateCommitted }
|
||||
public enum NestJobStage
|
||||
{
|
||||
EvaluatingCandidate,
|
||||
PlateCommitted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whole-job progress. Counts change only after a physical sheet commits; LegacyProgress is optional
|
||||
/// non-authoritative detail from a plate nester while its candidate remains under evaluation.
|
||||
/// </summary>
|
||||
public sealed record NestJobProgress(NestJobStage Stage, string StockId, int PlateIndex,
|
||||
int CommittedPlates, int CommittedParts, NestProgress LegacyProgress = null);
|
||||
public sealed record NestJobProgress(
|
||||
NestJobStage Stage,
|
||||
string StockId,
|
||||
int PlateIndex,
|
||||
int CommittedPlates,
|
||||
int CommittedParts,
|
||||
NestProgress LegacyProgress = null
|
||||
);
|
||||
|
||||
@@ -3,15 +3,32 @@ using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum NestJobStatus { Complete, Incomplete }
|
||||
public enum NestJobStopReason { Completed, StockExhausted, NoPlacementFound, PlateLimitReached }
|
||||
public enum NestJobStatus
|
||||
{
|
||||
Complete,
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
public enum NestJobStopReason
|
||||
{
|
||||
Completed,
|
||||
StockExhausted,
|
||||
NoPlacementFound,
|
||||
PlateLimitReached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate about the snapshot origin, then translate by X/Y into the selected plate quadrant frame.
|
||||
/// Rotation is in radians. InstanceIndex is zero-based and unique within a part requirement across the job.
|
||||
/// The runner assigns final instance indices when committing a candidate.
|
||||
/// </summary>
|
||||
public sealed record NestJobPlacement(string PartId, int InstanceIndex, double X, double Y, double Rotation);
|
||||
public sealed record NestJobPlacement(
|
||||
string PartId,
|
||||
int InstanceIndex,
|
||||
double X,
|
||||
double Y,
|
||||
double Rotation
|
||||
);
|
||||
|
||||
/// <summary>Requested = Placed + Unplaced for a requirement ID.</summary>
|
||||
public sealed record PartFulfillment(string PartId, int Requested, int Placed, int Unplaced);
|
||||
@@ -22,7 +39,11 @@ public sealed record StockUsage(string StockId, int Used, int? Remaining);
|
||||
/// <summary>One physical sheet, with owned ordered placements and immutable stock/settings snapshot.</summary>
|
||||
public sealed class NestJobPlateResult
|
||||
{
|
||||
public NestJobPlateResult(int plateIndex, NestPlateStock stock, IEnumerable<NestJobPlacement> placements)
|
||||
public NestJobPlateResult(
|
||||
int plateIndex,
|
||||
NestPlateStock stock,
|
||||
IEnumerable<NestJobPlacement> placements
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stock);
|
||||
PlateIndex = plateIndex;
|
||||
@@ -39,9 +60,13 @@ public sealed class NestJobPlateResult
|
||||
/// <summary>Detached result values in commit/input order; no mutable Drawing, Plate, or NestItem escapes.</summary>
|
||||
public sealed class NestJobResult
|
||||
{
|
||||
public NestJobResult(NestJobStatus status, NestJobStopReason stopReason,
|
||||
IEnumerable<NestJobPlateResult> plates, IEnumerable<PartFulfillment> fulfillment,
|
||||
IEnumerable<StockUsage> stockUsage)
|
||||
public NestJobResult(
|
||||
NestJobStatus status,
|
||||
NestJobStopReason stopReason,
|
||||
IEnumerable<NestJobPlateResult> plates,
|
||||
IEnumerable<PartFulfillment> fulfillment,
|
||||
IEnumerable<StockUsage> stockUsage
|
||||
)
|
||||
{
|
||||
Status = status;
|
||||
StopReason = stopReason;
|
||||
|
||||
@@ -20,20 +20,32 @@ public sealed class NestJobRunner : INestingEngine
|
||||
this.plateNesterFactory = plateNesterFactory;
|
||||
}
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public NestJobResult Solve(
|
||||
NestJob job,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
NestJobValidator.Validate(job);
|
||||
var plates = new List<NestJobPlateResult>();
|
||||
var remaining = job.Parts.ToDictionary(part => part.Id, part => part.Quantity, StringComparer.Ordinal);
|
||||
var remaining = job.Parts.ToDictionary(
|
||||
part => part.Id,
|
||||
part => part.Quantity,
|
||||
StringComparer.Ordinal
|
||||
);
|
||||
var placed = job.Parts.ToDictionary(part => part.Id, _ => 0, StringComparer.Ordinal);
|
||||
var parts = job.Parts.ToDictionary(part => part.Id, StringComparer.Ordinal);
|
||||
var used = job.Plates.ToDictionary(stock => stock.Id, _ => 0, StringComparer.Ordinal);
|
||||
var comparer = new NestJobCandidateComparer(job.Parts);
|
||||
var nester = job.Parts.Count == 0 ? null : plateNesterFactory(job.Options.PlacementStrategy) ??
|
||||
throw new NotSupportedException($"Unknown placement strategy: {job.Options.PlacementStrategy}.");
|
||||
var nester =
|
||||
job.Parts.Count == 0
|
||||
? null
|
||||
: plateNesterFactory(job.Options.PlacementStrategy)
|
||||
?? throw new NotSupportedException(
|
||||
$"Unknown placement strategy: {job.Options.PlacementStrategy}."
|
||||
);
|
||||
var reason = NestJobStopReason.Completed;
|
||||
while (remaining.Values.Any(count => count > 0))
|
||||
{
|
||||
@@ -49,21 +61,55 @@ public sealed class NestJobRunner : INestingEngine
|
||||
for (var index = 0; index < job.Plates.Count; index++)
|
||||
{
|
||||
var stock = job.Plates[index];
|
||||
if (stock.Quantity is int quantity && used[stock.Id] >= quantity) continue;
|
||||
if (stock.Quantity is int quantity && used[stock.Id] >= quantity)
|
||||
continue;
|
||||
hasAvailableStock = true;
|
||||
var request = new PlatePlacementRequest(stock, job.Parts.Where(part => remaining[part.Id] > 0)
|
||||
.Select(part => new NestJobPart(part.Id, part.Geometry, remaining[part.Id], part.Priority, part.Rotation)));
|
||||
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id,
|
||||
plates.Count, plates.Count, placed.Values.Sum()));
|
||||
var request = new PlatePlacementRequest(
|
||||
stock,
|
||||
job.Parts.Where(part => remaining[part.Id] > 0)
|
||||
.Select(part => new NestJobPart(
|
||||
part.Id,
|
||||
part.Geometry,
|
||||
remaining[part.Id],
|
||||
part.Priority,
|
||||
part.Rotation
|
||||
))
|
||||
);
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.EvaluatingCandidate,
|
||||
stock.Id,
|
||||
plates.Count,
|
||||
plates.Count,
|
||||
placed.Values.Sum()
|
||||
)
|
||||
);
|
||||
token.ThrowIfCancellationRequested();
|
||||
var candidateProgress = progress == null ? null : new CandidateProgress(progress, stock.Id,
|
||||
plates.Count, plates.Count, placed.Values.Sum());
|
||||
var candidateProgress =
|
||||
progress == null
|
||||
? null
|
||||
: new CandidateProgress(
|
||||
progress,
|
||||
stock.Id,
|
||||
plates.Count,
|
||||
plates.Count,
|
||||
placed.Values.Sum()
|
||||
);
|
||||
var candidate = nester.Place(request, candidateProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
NestJobValidator.ValidateCandidate(candidate, stock, remaining, parts);
|
||||
var trial = new CandidateTrial(candidate, stock, index);
|
||||
if (winner == null || comparer.Compare(trial.Candidate, trial.Stock, trial.StockIndex,
|
||||
winner.Candidate, winner.Stock, winner.StockIndex) > 0)
|
||||
if (
|
||||
winner == null
|
||||
|| comparer.Compare(
|
||||
trial.Candidate,
|
||||
trial.Stock,
|
||||
trial.StockIndex,
|
||||
winner.Candidate,
|
||||
winner.Stock,
|
||||
winner.StockIndex
|
||||
) > 0
|
||||
)
|
||||
winner = trial;
|
||||
}
|
||||
|
||||
@@ -86,27 +132,65 @@ public sealed class NestJobRunner : INestingEngine
|
||||
}
|
||||
used[winner.Stock.Id]++;
|
||||
plates.Add(new NestJobPlateResult(plates.Count, winner.Stock, committed));
|
||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, winner.Stock.Id,
|
||||
plates.Count - 1, plates.Count, placed.Values.Sum()));
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.PlateCommitted,
|
||||
winner.Stock.Id,
|
||||
plates.Count - 1,
|
||||
plates.Count,
|
||||
placed.Values.Sum()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new NestJobResult(reason == NestJobStopReason.Completed ? NestJobStatus.Complete : NestJobStatus.Incomplete,
|
||||
reason, plates, job.Parts.Select(part => new PartFulfillment(part.Id, part.Quantity, placed[part.Id], remaining[part.Id])),
|
||||
job.Plates.Select(stock => new StockUsage(stock.Id, used[stock.Id],
|
||||
stock.Quantity is int quantity ? quantity - used[stock.Id] : null)));
|
||||
return new NestJobResult(
|
||||
reason == NestJobStopReason.Completed
|
||||
? NestJobStatus.Complete
|
||||
: NestJobStatus.Incomplete,
|
||||
reason,
|
||||
plates,
|
||||
job.Parts.Select(part => new PartFulfillment(
|
||||
part.Id,
|
||||
part.Quantity,
|
||||
placed[part.Id],
|
||||
remaining[part.Id]
|
||||
)),
|
||||
job.Plates.Select(stock => new StockUsage(
|
||||
stock.Id,
|
||||
used[stock.Id],
|
||||
stock.Quantity is int quantity ? quantity - used[stock.Id] : null
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
private sealed record CandidateTrial(PlateCandidate Candidate, NestPlateStock Stock, int StockIndex);
|
||||
private sealed record CandidateTrial(
|
||||
PlateCandidate Candidate,
|
||||
NestPlateStock Stock,
|
||||
int StockIndex
|
||||
);
|
||||
|
||||
private sealed class CandidateProgress(IProgress<NestJobProgress> progress, string stockId, int plateIndex,
|
||||
int committedPlates, int committedParts) : IProgress<NestJobProgress>
|
||||
private sealed class CandidateProgress(
|
||||
IProgress<NestJobProgress> progress,
|
||||
string stockId,
|
||||
int plateIndex,
|
||||
int committedPlates,
|
||||
int committedParts
|
||||
) : IProgress<NestJobProgress>
|
||||
{
|
||||
public void Report(NestJobProgress value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, plateIndex,
|
||||
committedPlates, committedParts, value.LegacyProgress));
|
||||
progress.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.EvaluatingCandidate,
|
||||
stockId,
|
||||
plateIndex,
|
||||
committedPlates,
|
||||
committedParts,
|
||||
value.LegacyProgress
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,35 +13,65 @@ public static class NestJobValidator
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
var edges = stock.EdgeSpacing;
|
||||
if (!Positive(stock.Size.Width) || !Positive(stock.Size.Length) ||
|
||||
!Nonnegative(stock.PartSpacing) || !Nonnegative(edges.Left) || !Nonnegative(edges.Right) ||
|
||||
!Nonnegative(edges.Top) || !Nonnegative(edges.Bottom) || stock.Quadrant < 1 || stock.Quadrant > 4 ||
|
||||
edges.Left + edges.Right >= stock.Size.Length || edges.Top + edges.Bottom >= stock.Size.Width)
|
||||
throw new ArgumentException($"Invalid stock dimensions/settings: {stock.Id}.", nameof(job));
|
||||
if (
|
||||
!Positive(stock.Size.Width)
|
||||
|| !Positive(stock.Size.Length)
|
||||
|| !Nonnegative(stock.PartSpacing)
|
||||
|| !Nonnegative(edges.Left)
|
||||
|| !Nonnegative(edges.Right)
|
||||
|| !Nonnegative(edges.Top)
|
||||
|| !Nonnegative(edges.Bottom)
|
||||
|| stock.Quadrant < 1
|
||||
|| stock.Quadrant > 4
|
||||
|| edges.Left + edges.Right >= stock.Size.Length
|
||||
|| edges.Top + edges.Bottom >= stock.Size.Width
|
||||
)
|
||||
throw new ArgumentException(
|
||||
$"Invalid stock dimensions/settings: {stock.Id}.",
|
||||
nameof(job)
|
||||
);
|
||||
}
|
||||
foreach (var part in job.Parts)
|
||||
{
|
||||
if (part.Geometry.Motions.Count == 0 || part.Geometry.Motions.Any(m =>
|
||||
!double.IsFinite(m.X) || !double.IsFinite(m.Y) ||
|
||||
!double.IsFinite(m.CenterX) || !double.IsFinite(m.CenterY)))
|
||||
throw new ArgumentException($"Geometry must contain finite motions: {part.Id}.", nameof(job));
|
||||
if (
|
||||
part.Geometry.Motions.Count == 0
|
||||
|| part.Geometry.Motions.Any(m =>
|
||||
!double.IsFinite(m.X)
|
||||
|| !double.IsFinite(m.Y)
|
||||
|| !double.IsFinite(m.CenterX)
|
||||
|| !double.IsFinite(m.CenterY)
|
||||
)
|
||||
)
|
||||
throw new ArgumentException(
|
||||
$"Geometry must contain finite motions: {part.Id}.",
|
||||
nameof(job)
|
||||
);
|
||||
try
|
||||
{
|
||||
NestJobPlacementValidator.ValidateGeometry(part.Geometry);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
throw new ArgumentException($"Geometry must contain usable closed edges: {part.Id}. {exception.Message}", nameof(job), exception);
|
||||
throw new ArgumentException(
|
||||
$"Geometry must contain usable closed edges: {part.Id}. {exception.Message}",
|
||||
nameof(job),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ValidateCandidate(PlateCandidate candidate, NestPlateStock stock,
|
||||
IReadOnlyDictionary<string, int> remaining, IReadOnlyDictionary<string, NestJobPart> parts)
|
||||
internal static void ValidateCandidate(
|
||||
PlateCandidate candidate,
|
||||
NestPlateStock stock,
|
||||
IReadOnlyDictionary<string, int> remaining,
|
||||
IReadOnlyDictionary<string, NestJobPart> parts
|
||||
)
|
||||
{
|
||||
NestJobPlacementValidator.ValidateCandidate(candidate, stock, remaining, parts);
|
||||
}
|
||||
|
||||
private static bool Positive(double value) => double.IsFinite(value) && value > 0;
|
||||
|
||||
private static bool Nonnegative(double value) => double.IsFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@ namespace OpenNest;
|
||||
/// <summary>Immutable stock settings. Size and spacing are copied value types, not caller-owned settings.</summary>
|
||||
public sealed class NestPlateStock
|
||||
{
|
||||
public NestPlateStock(string id, Size size, int? quantity = null, double partSpacing = 0,
|
||||
Spacing edgeSpacing = default, int quadrant = 1)
|
||||
public NestPlateStock(
|
||||
string id,
|
||||
Size size,
|
||||
int? quantity = null,
|
||||
double partSpacing = 0,
|
||||
Spacing edgeSpacing = default,
|
||||
int quadrant = 1
|
||||
)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
if (quantity < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Size = size;
|
||||
Quantity = quantity;
|
||||
@@ -21,6 +28,7 @@ public sealed class NestPlateStock
|
||||
|
||||
public string Id { get; }
|
||||
public Size Size { get; }
|
||||
|
||||
/// <summary>Available physical sheets: null is unlimited, zero is legal but unavailable.</summary>
|
||||
public int? Quantity { get; }
|
||||
public double PartSpacing { get; }
|
||||
|
||||
@@ -20,20 +20,35 @@ public static class NestingEngineRegistry
|
||||
|
||||
static NestingEngineRegistry()
|
||||
{
|
||||
Register("StockLadder", "Caller-stock constrained-first fill and equivalent-demand area repacking",
|
||||
() => new StockLadderNestingEngine());
|
||||
Register(
|
||||
"StockLadder",
|
||||
"Caller-stock constrained-first fill and equivalent-demand area repacking",
|
||||
() => new StockLadderNestingEngine()
|
||||
);
|
||||
|
||||
Register("Default", "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||
() => new FixedStrategyNestingEngine("Default"));
|
||||
Register(
|
||||
"Default",
|
||||
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||
() => new FixedStrategyNestingEngine("Default")
|
||||
);
|
||||
|
||||
Register("Strip", "Strip-based nesting for mixed-drawing layouts",
|
||||
() => new FixedStrategyNestingEngine("Strip"));
|
||||
Register(
|
||||
"Strip",
|
||||
"Strip-based nesting for mixed-drawing layouts",
|
||||
() => new FixedStrategyNestingEngine("Strip")
|
||||
);
|
||||
|
||||
Register("Vertical Remnant", "Optimizes for largest right-side vertical drop",
|
||||
() => new FixedStrategyNestingEngine("Vertical Remnant"));
|
||||
Register(
|
||||
"Vertical Remnant",
|
||||
"Optimizes for largest right-side vertical drop",
|
||||
() => new FixedStrategyNestingEngine("Vertical Remnant")
|
||||
);
|
||||
|
||||
Register("Horizontal Remnant", "Optimizes for largest top-side horizontal drop",
|
||||
() => new FixedStrategyNestingEngine("Horizontal Remnant"));
|
||||
Register(
|
||||
"Horizontal Remnant",
|
||||
"Optimizes for largest top-side horizontal drop",
|
||||
() => new FixedStrategyNestingEngine("Horizontal Remnant")
|
||||
);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<NestingEngineInfo> AvailableEngines => engines;
|
||||
@@ -73,24 +88,32 @@ public static class NestingEngineRegistry
|
||||
|
||||
if (ctor == null)
|
||||
{
|
||||
Debug.WriteLine($"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor");
|
||||
Debug.WriteLine(
|
||||
$"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Register(type.Name, string.Empty, () => (INestingEngine)ctor.Invoke(null));
|
||||
Debug.WriteLine($"[NestingEngineRegistry] Loaded plugin engine: {type.Name}");
|
||||
Debug.WriteLine(
|
||||
$"[NestingEngineRegistry] Loaded plugin engine: {type.Name}"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}");
|
||||
Debug.WriteLine(
|
||||
$"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}");
|
||||
Debug.WriteLine(
|
||||
$"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,16 @@ using OpenNest.CNC;
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Exact immutable CNC motion values. Rapid moves retain contour/hole boundaries; arcs are not tessellated.</summary>
|
||||
public sealed record PartGeometryMotion(CodeType Type, double X, double Y, double CenterX,
|
||||
double CenterY, RotationType Rotation, LayerType Layer, bool Suppressed);
|
||||
public sealed record PartGeometryMotion(
|
||||
CodeType Type,
|
||||
double X,
|
||||
double Y,
|
||||
double CenterX,
|
||||
double CenterY,
|
||||
RotationType Rotation,
|
||||
LayerType Layer,
|
||||
bool Suppressed
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Owned geometry only: no Drawing, quantity, events, or mutable CNC references are retained.
|
||||
@@ -29,16 +37,44 @@ public sealed class PartGeometrySnapshot
|
||||
public static PartGeometrySnapshot FromProgram(Program program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
var motions = program.Codes.Select(code => code switch
|
||||
{
|
||||
ArcMove arc => new PartGeometryMotion(arc.Type, arc.EndPoint.X, arc.EndPoint.Y,
|
||||
arc.CenterPoint.X, arc.CenterPoint.Y, arc.Rotation, arc.Layer, arc.Suppressed),
|
||||
LinearMove line => new PartGeometryMotion(line.Type, line.EndPoint.X, line.EndPoint.Y,
|
||||
0, 0, default, line.Layer, line.Suppressed),
|
||||
RapidMove rapid => new PartGeometryMotion(rapid.Type, rapid.EndPoint.X, rapid.EndPoint.Y,
|
||||
0, 0, default, default, rapid.Suppressed),
|
||||
_ => throw new NotSupportedException("Geometry snapshots currently support only flat rapid/linear/arc programs.")
|
||||
});
|
||||
var motions = program.Codes.Select(code =>
|
||||
code switch
|
||||
{
|
||||
ArcMove arc => new PartGeometryMotion(
|
||||
arc.Type,
|
||||
arc.EndPoint.X,
|
||||
arc.EndPoint.Y,
|
||||
arc.CenterPoint.X,
|
||||
arc.CenterPoint.Y,
|
||||
arc.Rotation,
|
||||
arc.Layer,
|
||||
arc.Suppressed
|
||||
),
|
||||
LinearMove line => new PartGeometryMotion(
|
||||
line.Type,
|
||||
line.EndPoint.X,
|
||||
line.EndPoint.Y,
|
||||
0,
|
||||
0,
|
||||
default,
|
||||
line.Layer,
|
||||
line.Suppressed
|
||||
),
|
||||
RapidMove rapid => new PartGeometryMotion(
|
||||
rapid.Type,
|
||||
rapid.EndPoint.X,
|
||||
rapid.EndPoint.Y,
|
||||
0,
|
||||
0,
|
||||
default,
|
||||
default,
|
||||
rapid.Suppressed
|
||||
),
|
||||
_ => throw new NotSupportedException(
|
||||
"Geometry snapshots currently support only flat rapid/linear/arc programs."
|
||||
),
|
||||
}
|
||||
);
|
||||
return new PartGeometrySnapshot(program.Mode, motions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,20 +21,25 @@ public sealed class DefaultPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Func<Plate, DefaultNestEngine> engineFactory;
|
||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
|
||||
public DefaultPlateNester() : this(static plate => new DefaultNestEngine(plate))
|
||||
{
|
||||
}
|
||||
public DefaultPlateNester()
|
||||
: this(static plate => new DefaultNestEngine(plate)) { }
|
||||
|
||||
/// <param name="engineFactory">Injectable for tests; defaults to <see cref="DefaultNestEngine"/>.</param>
|
||||
public DefaultPlateNester(Func<Plate, DefaultNestEngine> engineFactory)
|
||||
{
|
||||
this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
this.engineFactory =
|
||||
engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
}
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public PlateCandidate Place(
|
||||
PlatePlacementRequest request,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -52,29 +57,38 @@ public sealed class DefaultPlateNester : IPlateNester
|
||||
|
||||
// Quantity is the request's remaining demand; the engine may mutate this per-trial item,
|
||||
// and that mutation is deliberately discarded — placement counts come from the result.
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End
|
||||
});
|
||||
items.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var engine =
|
||||
engineFactory(plate)
|
||||
?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id);
|
||||
var parts = engine.Nest(items, legacyProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (parts == null) throw new InvalidOperationException("Engine returned null placements.");
|
||||
if (parts == null)
|
||||
throw new InvalidOperationException("Engine returned null placements.");
|
||||
|
||||
var placements = new List<NestJobPlacement>(parts.Count);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id))
|
||||
throw new InvalidOperationException("Placement does not reference a known requirement drawing.");
|
||||
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
throw new InvalidOperationException(
|
||||
"Placement does not reference a known requirement drawing."
|
||||
);
|
||||
placements.Add(
|
||||
new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)
|
||||
);
|
||||
}
|
||||
|
||||
return new PlateCandidate(placements);
|
||||
|
||||
@@ -13,8 +13,11 @@ internal sealed class OrderedPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Dictionary<string, Drawing> drawings = new(StringComparer.Ordinal);
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public PlateCandidate Place(
|
||||
PlatePlacementRequest request,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var work = DrawingJobMapper.CreatePlate(request.Stock).WorkArea();
|
||||
var poses = new List<NestJobPlacement>();
|
||||
@@ -38,27 +41,55 @@ internal sealed class OrderedPlateNester : IPlateNester
|
||||
token.ThrowIfCancellationRequested();
|
||||
// FillLinear uses actual line/arc geometry for copy distances.
|
||||
var parts = new FillLinear(region, request.Stock.PartSpacing)
|
||||
.Fill(drawing, angle, NestDirection.Horizontal).Take(left).ToList();
|
||||
if (parts.Count == 0 || (best != null && parts.Count <= best.Count)) continue;
|
||||
var trial = poses.Concat(parts.Select(p => new NestJobPlacement(requirement.Id, 0,
|
||||
p.Location.X, p.Location.Y, p.Rotation))).ToList();
|
||||
.Fill(drawing, angle, NestDirection.Horizontal)
|
||||
.Take(left)
|
||||
.ToList();
|
||||
if (parts.Count == 0 || (best != null && parts.Count <= best.Count))
|
||||
continue;
|
||||
var trial = poses
|
||||
.Concat(
|
||||
parts.Select(p => new NestJobPlacement(
|
||||
requirement.Id,
|
||||
0,
|
||||
p.Location.X,
|
||||
p.Location.Y,
|
||||
p.Rotation
|
||||
))
|
||||
)
|
||||
.ToList();
|
||||
try
|
||||
{
|
||||
NestJobValidator.ValidateCandidate(new PlateCandidate(trial), request.Stock, demand, requirements);
|
||||
NestJobValidator.ValidateCandidate(
|
||||
new PlateCandidate(trial),
|
||||
request.Stock,
|
||||
demand,
|
||||
requirements
|
||||
);
|
||||
best = parts;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Geometry kernels are proposal generators, never the acceptance gate.
|
||||
}
|
||||
if (best?.Count == left) break;
|
||||
if (best?.Count == left)
|
||||
break;
|
||||
}
|
||||
if (best?.Count == left) break;
|
||||
if (best?.Count == left)
|
||||
break;
|
||||
}
|
||||
if (best == null) break;
|
||||
if (best == null)
|
||||
break;
|
||||
foreach (var part in best)
|
||||
{
|
||||
poses.Add(new NestJobPlacement(requirement.Id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
poses.Add(
|
||||
new NestJobPlacement(
|
||||
requirement.Id,
|
||||
0,
|
||||
part.Location.X,
|
||||
part.Location.Y,
|
||||
part.Rotation
|
||||
)
|
||||
);
|
||||
obstacles.Add(part.BoundingBox.Offset(request.Stock.PartSpacing));
|
||||
}
|
||||
left -= best.Count;
|
||||
@@ -83,13 +114,15 @@ internal sealed class OrderedPlateNester : IPlateNester
|
||||
yield return System.Math.PI;
|
||||
yield return 3 * System.Math.PI / 2;
|
||||
for (var degrees = 5; degrees < 180; degrees += 5)
|
||||
if (degrees != 90) yield return degrees * System.Math.PI / 180;
|
||||
if (degrees != 90)
|
||||
yield return degrees * System.Math.PI / 180;
|
||||
yield break;
|
||||
}
|
||||
for (var index = 0L; ; index++)
|
||||
{
|
||||
var angle = policy.Start + index * policy.Step;
|
||||
if (angle > policy.End + 1e-9) yield break;
|
||||
if (angle > policy.End + 1e-9)
|
||||
yield break;
|
||||
yield return angle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,25 @@ public sealed class StripPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Func<Plate, StripNestEngine> engineFactory;
|
||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
|
||||
public StripPlateNester() : this(static plate => new StripNestEngine(plate))
|
||||
{
|
||||
}
|
||||
public StripPlateNester()
|
||||
: this(static plate => new StripNestEngine(plate)) { }
|
||||
|
||||
/// <param name="engineFactory">Injectable for tests; defaults to <see cref="StripNestEngine"/>.</param>
|
||||
public StripPlateNester(Func<Plate, StripNestEngine> engineFactory)
|
||||
{
|
||||
this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
this.engineFactory =
|
||||
engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
}
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public PlateCandidate Place(
|
||||
PlatePlacementRequest request,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -47,29 +52,38 @@ public sealed class StripPlateNester : IPlateNester
|
||||
idByDrawing.Add(drawing, requirement.Id);
|
||||
}
|
||||
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End
|
||||
});
|
||||
items.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var engine =
|
||||
engineFactory(plate)
|
||||
?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id);
|
||||
var parts = engine.Nest(items, legacyProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (parts == null) throw new InvalidOperationException("Engine returned null placements.");
|
||||
if (parts == null)
|
||||
throw new InvalidOperationException("Engine returned null placements.");
|
||||
|
||||
var placements = new List<NestJobPlacement>(parts.Count);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id))
|
||||
throw new InvalidOperationException("Placement does not reference a known requirement drawing.");
|
||||
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
throw new InvalidOperationException(
|
||||
"Placement does not reference a known requirement drawing."
|
||||
);
|
||||
placements.Add(
|
||||
new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)
|
||||
);
|
||||
}
|
||||
|
||||
return new PlateCandidate(placements);
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace OpenNest;
|
||||
/// <summary>Owned candidate poses only; not committed fulfillment or inventory accounting.</summary>
|
||||
public sealed class PlateCandidate
|
||||
{
|
||||
public PlateCandidate(IEnumerable<NestJobPlacement> placements) => Placements = NestJob.Own(placements);
|
||||
public PlateCandidate(IEnumerable<NestJobPlacement> placements) =>
|
||||
Placements = NestJob.Own(placements);
|
||||
|
||||
public IReadOnlyList<NestJobPlacement> Placements { get; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,9 +17,13 @@ public static class PlateNesterFactory
|
||||
{
|
||||
"Default" => new DefaultPlateNester(),
|
||||
"Strip" => new StripPlateNester(),
|
||||
"Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(plate)),
|
||||
"Horizontal Remnant" => new LegacyPlateNesterAdapter(plate => new HorizontalRemnantEngine(plate)),
|
||||
_ => throw new NotSupportedException($"Unknown placement strategy: {strategy}.")
|
||||
"Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(
|
||||
plate
|
||||
)),
|
||||
"Horizontal Remnant" => new LegacyPlateNesterAdapter(
|
||||
plate => new HorizontalRemnantEngine(plate)
|
||||
),
|
||||
_ => throw new NotSupportedException($"Unknown placement strategy: {strategy}."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum RotationPolicyKind { Fixed, BoundedSweep, Automatic }
|
||||
public enum RotationPolicyKind
|
||||
{
|
||||
Fixed,
|
||||
BoundedSweep,
|
||||
Automatic,
|
||||
}
|
||||
|
||||
/// <summary>Immutable rotation constraints, in radians about the geometry origin.</summary>
|
||||
public sealed class RotationPolicy
|
||||
@@ -22,14 +27,21 @@ public sealed class RotationPolicy
|
||||
public double End { get; }
|
||||
public double Step { get; }
|
||||
public static RotationPolicy Automatic { get; } = new(RotationPolicyKind.Automatic, 0, 0, 0);
|
||||
public static RotationPolicy Fixed(double angle) => new(RotationPolicyKind.Fixed, angle, angle, 0);
|
||||
|
||||
public static RotationPolicy Fixed(double angle) =>
|
||||
new(RotationPolicyKind.Fixed, angle, angle, 0);
|
||||
|
||||
public static RotationPolicy BoundedSweep(double start, double end, double step)
|
||||
{
|
||||
if (step <= 0 || end < start) throw new ArgumentException("Sweep needs a positive step and ordered bounds.");
|
||||
if (step <= 0 || end < start)
|
||||
throw new ArgumentException("Sweep needs a positive step and ordered bounds.");
|
||||
return new RotationPolicy(RotationPolicyKind.BoundedSweep, start, end, step);
|
||||
}
|
||||
|
||||
/// <summary>Preserves the legacy zero-step automatic sentinel; zero never means locked rotation.</summary>
|
||||
public static RotationPolicy FromLegacy(double stepAngle, double rotationStart, double rotationEnd) =>
|
||||
stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle);
|
||||
public static RotationPolicy FromLegacy(
|
||||
double stepAngle,
|
||||
double rotationStart,
|
||||
double rotationEnd
|
||||
) => stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,18 @@ namespace OpenNest;
|
||||
public sealed class StockLadderNestingEngine : INestingEngine
|
||||
{
|
||||
private readonly Func<IPlateNester> factory;
|
||||
public StockLadderNestingEngine() : this(() => new OrderedPlateNester()) { }
|
||||
|
||||
public StockLadderNestingEngine()
|
||||
: this(() => new OrderedPlateNester()) { }
|
||||
|
||||
public StockLadderNestingEngine(Func<IPlateNester> factory) =>
|
||||
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public NestJobResult Solve(
|
||||
NestJob job,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -33,32 +39,41 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
|
||||
// Probe actual validated single-part placements, not bounding-box fit assertions.
|
||||
foreach (var part in job.Parts)
|
||||
foreach (var stock in job.Plates.Where(s => s.Quantity != 0))
|
||||
{
|
||||
var probe = Trial(stock, new[] { WithQuantity(part, 1) });
|
||||
if (probe.Placements.Count != 0) feasible[part.Id].Add(stock.Id);
|
||||
}
|
||||
var ordered = job.Parts.OrderBy(p => p.Priority)
|
||||
.ThenBy(p => feasible[p.Id].Count).ThenByDescending(p => areas[p.Id]).ToList();
|
||||
foreach (var stock in job.Plates.Where(s => s.Quantity != 0))
|
||||
{
|
||||
var probe = Trial(stock, new[] { WithQuantity(part, 1) });
|
||||
if (probe.Placements.Count != 0)
|
||||
feasible[part.Id].Add(stock.Id);
|
||||
}
|
||||
var ordered = job
|
||||
.Parts.OrderBy(p => p.Priority)
|
||||
.ThenBy(p => feasible[p.Id].Count)
|
||||
.ThenByDescending(p => areas[p.Id])
|
||||
.ToList();
|
||||
var reason = NestJobStopReason.Completed;
|
||||
while (remaining.Values.Any(n => n > 0))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (job.Options.MaxPlates <= sheets.Count)
|
||||
{
|
||||
if (Consolidate()) continue;
|
||||
if (Consolidate())
|
||||
continue;
|
||||
reason = NestJobStopReason.PlateLimitReached;
|
||||
break;
|
||||
}
|
||||
var available = job.Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity).ToList();
|
||||
var available = job
|
||||
.Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity)
|
||||
.ToList();
|
||||
if (available.Count == 0)
|
||||
{
|
||||
if (Consolidate()) continue;
|
||||
if (Consolidate())
|
||||
continue;
|
||||
reason = NestJobStopReason.StockExhausted;
|
||||
break;
|
||||
}
|
||||
var anchor = ordered.FirstOrDefault(p => remaining[p.Id] > 0 &&
|
||||
available.Any(s => feasible[p.Id].Contains(s.Id)));
|
||||
var anchor = ordered.FirstOrDefault(p =>
|
||||
remaining[p.Id] > 0 && available.Any(s => feasible[p.Id].Contains(s.Id))
|
||||
);
|
||||
if (anchor == null)
|
||||
{
|
||||
reason = NestJobStopReason.NoPlacementFound;
|
||||
@@ -69,14 +84,18 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
foreach (var stock in available.Where(s => feasible[anchor.Id].Contains(s.Id)))
|
||||
{
|
||||
// Pin the constrained anchor before fillers, including quantity-one requirements.
|
||||
var requests = new[] { anchor }.Concat(ordered.Where(p => p.Id != anchor.Id))
|
||||
.Where(p => remaining[p.Id] > 0).Select(p => WithQuantity(p, remaining[p.Id]));
|
||||
var requests = new[] { anchor }
|
||||
.Concat(ordered.Where(p => p.Id != anchor.Id))
|
||||
.Where(p => remaining[p.Id] > 0)
|
||||
.Select(p => WithQuantity(p, remaining[p.Id]));
|
||||
var candidate = Trial(stock, requests);
|
||||
if (!candidate.Placements.Any(p => p.PartId == anchor.Id)) continue;
|
||||
if (!candidate.Placements.Any(p => p.PartId == anchor.Id))
|
||||
continue;
|
||||
var sheet = new NestJobPlateResult(sheets.Count, stock, candidate.Placements);
|
||||
// Initial construction only: material area, never raw part counts. Repacking below
|
||||
// compares EXACTLY equivalent demand, and never replaces a sheet by a partial fill.
|
||||
var value = EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]);
|
||||
var value =
|
||||
EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]);
|
||||
if (value < score - 1e-9)
|
||||
{
|
||||
winner = sheet;
|
||||
@@ -90,28 +109,69 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
}
|
||||
sheets.Add(winner);
|
||||
used[winner.StockId]++;
|
||||
foreach (var pose in winner.Placements) remaining[pose.PartId]--;
|
||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, winner.StockId,
|
||||
sheets.Count - 1, sheets.Count, sheets.Sum(s => s.Placements.Count)));
|
||||
foreach (var pose in winner.Placements)
|
||||
remaining[pose.PartId]--;
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.PlateCommitted,
|
||||
winner.StockId,
|
||||
sheets.Count - 1,
|
||||
sheets.Count,
|
||||
sheets.Sum(s => s.Placements.Count)
|
||||
)
|
||||
);
|
||||
}
|
||||
Consolidate();
|
||||
token.ThrowIfCancellationRequested();
|
||||
var placed = job.Parts.ToDictionary(p => p.Id, _ => 0);
|
||||
var final = sheets.Select((sheet, index) => new NestJobPlateResult(index, sheet.Stock,
|
||||
sheet.Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ }).ToList())).ToList();
|
||||
return new NestJobResult(reason == NestJobStopReason.Completed ? NestJobStatus.Complete : NestJobStatus.Incomplete,
|
||||
reason, final, job.Parts.Select(p => new PartFulfillment(p.Id, p.Quantity, placed[p.Id], remaining[p.Id])),
|
||||
job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id])));
|
||||
var final = sheets
|
||||
.Select(
|
||||
(sheet, index) =>
|
||||
new NestJobPlateResult(
|
||||
index,
|
||||
sheet.Stock,
|
||||
sheet
|
||||
.Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ })
|
||||
.ToList()
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
return new NestJobResult(
|
||||
reason == NestJobStopReason.Completed
|
||||
? NestJobStatus.Complete
|
||||
: NestJobStatus.Incomplete,
|
||||
reason,
|
||||
final,
|
||||
job.Parts.Select(p => new PartFulfillment(
|
||||
p.Id,
|
||||
p.Quantity,
|
||||
placed[p.Id],
|
||||
remaining[p.Id]
|
||||
)),
|
||||
job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id]))
|
||||
);
|
||||
|
||||
PlateCandidate Trial(NestPlateStock stock, IEnumerable<NestJobPart> requirements)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var request = new PlatePlacementRequest(stock, requirements);
|
||||
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id,
|
||||
sheets.Count, sheets.Count, sheets.Sum(s => s.Placements.Count)));
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.EvaluatingCandidate,
|
||||
stock.Id,
|
||||
sheets.Count,
|
||||
sheets.Count,
|
||||
sheets.Sum(s => s.Placements.Count)
|
||||
)
|
||||
);
|
||||
var candidate = nester.Place(request, null, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
NestJobValidator.ValidateCandidate(candidate, stock, request.Parts.ToDictionary(p => p.Id, p => p.Quantity), parts);
|
||||
NestJobValidator.ValidateCandidate(
|
||||
candidate,
|
||||
stock,
|
||||
request.Parts.ToDictionary(p => p.Id, p => p.Quantity),
|
||||
parts
|
||||
);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -120,40 +180,55 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
var changed = false;
|
||||
// Single downgrade and adjacent pair merge only: bounded local search, no combinatorial tree.
|
||||
for (var index = 0; index < sheets.Count; index++)
|
||||
for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--)
|
||||
for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--)
|
||||
{
|
||||
var old = sheets.Skip(index).Take(count).ToList();
|
||||
var demand = old.SelectMany(s => s.Placements)
|
||||
.GroupBy(p => p.PartId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
var baseline = old.Sum(s => EstimateNetArea(job, s));
|
||||
NestJobPlateResult replacement = null;
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
var old = sheets.Skip(index).Take(count).ToList();
|
||||
var demand = old.SelectMany(s => s.Placements).GroupBy(p => p.PartId)
|
||||
token.ThrowIfCancellationRequested();
|
||||
var returned = old.Count(s => s.StockId == stock.Id);
|
||||
if (stock.Quantity is int limit && used[stock.Id] - returned >= limit)
|
||||
continue;
|
||||
// Even the maximum possible salvage credit cannot beat the incumbent.
|
||||
var lowerBound =
|
||||
stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate);
|
||||
if (lowerBound >= baseline - 1e-9)
|
||||
continue;
|
||||
if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id)))
|
||||
continue;
|
||||
var candidate = Trial(
|
||||
stock,
|
||||
ordered
|
||||
.Where(p => demand.ContainsKey(p.Id))
|
||||
.Select(p => WithQuantity(p, demand[p.Id]))
|
||||
);
|
||||
var actual = candidate
|
||||
.Placements.GroupBy(p => p.PartId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
var baseline = old.Sum(s => EstimateNetArea(job, s));
|
||||
NestJobPlateResult replacement = null;
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var returned = old.Count(s => s.StockId == stock.Id);
|
||||
if (stock.Quantity is int limit && used[stock.Id] - returned >= limit) continue;
|
||||
// Even the maximum possible salvage credit cannot beat the incumbent.
|
||||
var lowerBound = stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate);
|
||||
if (lowerBound >= baseline - 1e-9) continue;
|
||||
if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id))) continue;
|
||||
var candidate = Trial(stock, ordered.Where(p => demand.ContainsKey(p.Id))
|
||||
.Select(p => WithQuantity(p, demand[p.Id])));
|
||||
var actual = candidate.Placements.GroupBy(p => p.PartId).ToDictionary(g => g.Key, g => g.Count());
|
||||
if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value)) continue;
|
||||
var trial = new NestJobPlateResult(index, stock, candidate.Placements);
|
||||
var cost = EstimateNetArea(job, trial);
|
||||
if (cost >= baseline - 1e-9) continue;
|
||||
baseline = cost;
|
||||
replacement = trial;
|
||||
}
|
||||
if (replacement == null) continue;
|
||||
// No accounting changes until the entire equivalent-demand candidate is valid.
|
||||
foreach (var sheet in old) used[sheet.StockId]--;
|
||||
used[replacement.StockId]++;
|
||||
sheets.RemoveRange(index, count);
|
||||
sheets.Insert(index, replacement);
|
||||
changed = true;
|
||||
if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value))
|
||||
continue;
|
||||
var trial = new NestJobPlateResult(index, stock, candidate.Placements);
|
||||
var cost = EstimateNetArea(job, trial);
|
||||
if (cost >= baseline - 1e-9)
|
||||
continue;
|
||||
baseline = cost;
|
||||
replacement = trial;
|
||||
}
|
||||
if (replacement == null)
|
||||
continue;
|
||||
// No accounting changes until the entire equivalent-demand candidate is valid.
|
||||
foreach (var sheet in old)
|
||||
used[sheet.StockId]--;
|
||||
used[replacement.StockId]++;
|
||||
sheets.RemoveRange(index, count);
|
||||
sheets.Insert(index, replacement);
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -169,27 +244,33 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
{
|
||||
var area = sheet.Stock.Size.Width * sheet.Stock.Size.Length;
|
||||
var minimum = job.Options.MinimumSalvageDimension;
|
||||
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0) return area;
|
||||
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0)
|
||||
return area;
|
||||
var work = DrawingJobMapper.CreatePlate(sheet.Stock).WorkArea();
|
||||
var parts = job.Parts.ToDictionary(p => p.Id);
|
||||
var boxes = sheet.Placements.Select(p =>
|
||||
{
|
||||
var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId]));
|
||||
part.Rotate(p.Rotation);
|
||||
part.Location = new OpenNest.Geometry.Vector(p.X, p.Y);
|
||||
part.UpdateBounds();
|
||||
return part.BoundingBox;
|
||||
}).ToList();
|
||||
var boxes = sheet
|
||||
.Placements.Select(p =>
|
||||
{
|
||||
var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId]));
|
||||
part.Rotate(p.Rotation);
|
||||
part.Location = new OpenNest.Geometry.Vector(p.X, p.Y);
|
||||
part.UpdateBounds();
|
||||
return part.BoundingBox;
|
||||
})
|
||||
.ToList();
|
||||
var gap = sheet.Stock.PartSpacing;
|
||||
var candidates = new[]
|
||||
{
|
||||
(work.Length, boxes.Min(b => b.Bottom) - work.Bottom - gap),
|
||||
(work.Length, work.Top - boxes.Max(b => b.Top) - gap),
|
||||
(boxes.Min(b => b.Left) - work.Left - gap, work.Width),
|
||||
(work.Right - boxes.Max(b => b.Right) - gap, work.Width)
|
||||
(work.Right - boxes.Max(b => b.Right) - gap, work.Width),
|
||||
};
|
||||
var salvage = candidates.Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
|
||||
.Select(c => c.Item1 * c.Item2).DefaultIfEmpty(0).Max();
|
||||
var salvage = candidates
|
||||
.Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
|
||||
.Select(c => c.Item1 * c.Item2)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
return area - job.Options.SalvageRate * salvage;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user