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:
@@ -16,8 +16,11 @@ namespace OpenNest.Engine.BestFit
|
||||
public static Func<ISlideComputer> CreateSlideComputer { get; set; }
|
||||
|
||||
public static List<BestFitResult> GetOrCompute(
|
||||
Drawing drawing, double plateWidth, double plateHeight,
|
||||
double spacing)
|
||||
Drawing drawing,
|
||||
double plateWidth,
|
||||
double plateHeight,
|
||||
double spacing
|
||||
)
|
||||
{
|
||||
var key = new CacheKey(drawing, plateWidth, plateHeight, spacing);
|
||||
|
||||
@@ -34,14 +37,24 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
if (CreateEvaluator != null)
|
||||
{
|
||||
try { evaluator = CreateEvaluator(canonical, spacing); }
|
||||
catch { /* fall back to default evaluator */ }
|
||||
try
|
||||
{
|
||||
evaluator = CreateEvaluator(canonical, spacing);
|
||||
}
|
||||
catch
|
||||
{ /* fall back to default evaluator */
|
||||
}
|
||||
}
|
||||
|
||||
if (CreateSlideComputer != null)
|
||||
{
|
||||
try { slideComputer = CreateSlideComputer(); }
|
||||
catch { /* fall back to CPU slide computation */ }
|
||||
try
|
||||
{
|
||||
slideComputer = CreateSlideComputer();
|
||||
}
|
||||
catch
|
||||
{ /* fall back to CPU slide computation */
|
||||
}
|
||||
}
|
||||
|
||||
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer);
|
||||
@@ -58,8 +71,10 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
|
||||
public static void ComputeForSizes(
|
||||
Drawing drawing, double spacing,
|
||||
IEnumerable<(double Width, double Height)> plateSizes)
|
||||
Drawing drawing,
|
||||
double spacing,
|
||||
IEnumerable<(double Width, double Height)> plateSizes
|
||||
)
|
||||
{
|
||||
// Skip sizes that are already cached.
|
||||
var needed = new List<(double Width, double Height)>();
|
||||
@@ -80,8 +95,10 @@ namespace OpenNest.Engine.BestFit
|
||||
var maxHeight = 0.0;
|
||||
foreach (var size in needed)
|
||||
{
|
||||
if (size.Width > maxWidth) maxWidth = size.Width;
|
||||
if (size.Height > maxHeight) maxHeight = size.Height;
|
||||
if (size.Width > maxWidth)
|
||||
maxWidth = size.Width;
|
||||
if (size.Height > maxHeight)
|
||||
maxHeight = size.Height;
|
||||
}
|
||||
|
||||
IPairEvaluator evaluator = null;
|
||||
@@ -94,14 +111,24 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
if (CreateEvaluator != null)
|
||||
{
|
||||
try { evaluator = CreateEvaluator(canonical, spacing); }
|
||||
catch { /* fall back to default evaluator */ }
|
||||
try
|
||||
{
|
||||
evaluator = CreateEvaluator(canonical, spacing);
|
||||
}
|
||||
catch
|
||||
{ /* fall back to default evaluator */
|
||||
}
|
||||
}
|
||||
|
||||
if (CreateSlideComputer != null)
|
||||
{
|
||||
try { slideComputer = CreateSlideComputer(); }
|
||||
catch { /* fall back to CPU slide computation */ }
|
||||
try
|
||||
{
|
||||
slideComputer = CreateSlideComputer();
|
||||
}
|
||||
catch
|
||||
{ /* fall back to CPU slide computation */
|
||||
}
|
||||
}
|
||||
|
||||
// Compute candidates and evaluate once with the largest plate.
|
||||
@@ -114,25 +141,27 @@ namespace OpenNest.Engine.BestFit
|
||||
var filter = new BestFitFilter
|
||||
{
|
||||
MaxPlateWidth = size.Width,
|
||||
MaxPlateHeight = size.Height
|
||||
MaxPlateHeight = size.Height,
|
||||
};
|
||||
|
||||
var copy = new List<BestFitResult>(baseResults.Count);
|
||||
for (var i = 0; i < baseResults.Count; i++)
|
||||
{
|
||||
var r = baseResults[i];
|
||||
copy.Add(new BestFitResult
|
||||
{
|
||||
Candidate = r.Candidate,
|
||||
RotatedArea = r.RotatedArea,
|
||||
BoundingWidth = r.BoundingWidth,
|
||||
BoundingHeight = r.BoundingHeight,
|
||||
OptimalRotation = r.OptimalRotation,
|
||||
TrueArea = r.TrueArea,
|
||||
HullAngles = r.HullAngles,
|
||||
Keep = r.Keep,
|
||||
Reason = r.Reason
|
||||
});
|
||||
copy.Add(
|
||||
new BestFitResult
|
||||
{
|
||||
Candidate = r.Candidate,
|
||||
RotatedArea = r.RotatedArea,
|
||||
BoundingWidth = r.BoundingWidth,
|
||||
BoundingHeight = r.BoundingHeight,
|
||||
OptimalRotation = r.OptimalRotation,
|
||||
TrueArea = r.TrueArea,
|
||||
HullAngles = r.HullAngles,
|
||||
Keep = r.Keep,
|
||||
Reason = r.Reason,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
filter.Apply(copy);
|
||||
@@ -156,8 +185,13 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
}
|
||||
|
||||
public static void Populate(Drawing drawing, double plateWidth, double plateHeight,
|
||||
double spacing, List<BestFitResult> results)
|
||||
public static void Populate(
|
||||
Drawing drawing,
|
||||
double plateWidth,
|
||||
double plateHeight,
|
||||
double spacing,
|
||||
List<BestFitResult> results
|
||||
)
|
||||
{
|
||||
if (results == null || results.Count == 0)
|
||||
return;
|
||||
@@ -166,8 +200,10 @@ namespace OpenNest.Engine.BestFit
|
||||
_cache.TryAdd(key, results);
|
||||
}
|
||||
|
||||
public static Dictionary<(double PlateWidth, double PlateHeight, double Spacing), List<BestFitResult>>
|
||||
GetAllForDrawing(Drawing drawing)
|
||||
public static Dictionary<
|
||||
(double PlateWidth, double PlateHeight, double Spacing),
|
||||
List<BestFitResult>
|
||||
> GetAllForDrawing(Drawing drawing)
|
||||
{
|
||||
var result = new Dictionary<(double, double, double), List<BestFitResult>>();
|
||||
foreach (var kvp in _cache)
|
||||
@@ -200,10 +236,10 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
public bool Equals(CacheKey other)
|
||||
{
|
||||
return ReferenceEquals(Drawing, other.Drawing) &&
|
||||
PlateWidth == other.PlateWidth &&
|
||||
PlateHeight == other.PlateHeight &&
|
||||
Spacing == other.Spacing;
|
||||
return ReferenceEquals(Drawing, other.Drawing)
|
||||
&& PlateWidth == other.PlateWidth
|
||||
&& PlateHeight == other.PlateHeight
|
||||
&& Spacing == other.Spacing;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) => obj is CacheKey other && Equals(other);
|
||||
|
||||
@@ -17,8 +17,10 @@ namespace OpenNest.Engine.BestFit
|
||||
if (!result.Keep)
|
||||
continue;
|
||||
|
||||
if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight) ||
|
||||
result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight))
|
||||
if (
|
||||
result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight)
|
||||
|| result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight)
|
||||
)
|
||||
{
|
||||
result.Keep = false;
|
||||
result.Reason = "Exceeds plate dimensions";
|
||||
@@ -30,14 +32,21 @@ namespace OpenNest.Engine.BestFit
|
||||
if (aspect > MaxAspectRatio && result.Utilization < UtilizationOverride)
|
||||
{
|
||||
result.Keep = false;
|
||||
result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio);
|
||||
result.Reason = string.Format(
|
||||
"Aspect ratio {0:F1} exceeds max {1}",
|
||||
aspect,
|
||||
MaxAspectRatio
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Utilization < MinUtilization)
|
||||
{
|
||||
result.Keep = false;
|
||||
result.Reason = string.Format("Utilization {0:P0} below minimum", result.Utilization);
|
||||
result.Reason = string.Format(
|
||||
"Utilization {0:P0} below minimum",
|
||||
result.Utilization
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit.Tiling;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit.Tiling;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -16,20 +16,26 @@ namespace OpenNest.Engine.BestFit
|
||||
private readonly IDistanceComputer _distanceComputer;
|
||||
private readonly BestFitFilter _filter;
|
||||
|
||||
public BestFitFinder(double maxPlateWidth, double maxPlateHeight,
|
||||
IPairEvaluator evaluator = null, ISlideComputer slideComputer = null)
|
||||
public BestFitFinder(
|
||||
double maxPlateWidth,
|
||||
double maxPlateHeight,
|
||||
IPairEvaluator evaluator = null,
|
||||
ISlideComputer slideComputer = null
|
||||
)
|
||||
{
|
||||
_evaluator = evaluator ?? new PairEvaluator();
|
||||
_distanceComputer = slideComputer != null
|
||||
? (IDistanceComputer)new GpuDistanceComputer(slideComputer)
|
||||
: new CpuDistanceComputer();
|
||||
var plateAspect = System.Math.Max(maxPlateWidth, maxPlateHeight) /
|
||||
System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
|
||||
_distanceComputer =
|
||||
slideComputer != null
|
||||
? (IDistanceComputer)new GpuDistanceComputer(slideComputer)
|
||||
: new CpuDistanceComputer();
|
||||
var plateAspect =
|
||||
System.Math.Max(maxPlateWidth, maxPlateHeight)
|
||||
/ System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
|
||||
_filter = new BestFitFilter
|
||||
{
|
||||
MaxPlateWidth = maxPlateWidth,
|
||||
MaxPlateHeight = maxPlateHeight,
|
||||
MaxAspectRatio = System.Math.Max(5.0, plateAspect)
|
||||
MaxAspectRatio = System.Math.Max(5.0, plateAspect),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,20 +43,26 @@ namespace OpenNest.Engine.BestFit
|
||||
Drawing drawing,
|
||||
double spacing = 0.25,
|
||||
double stepSize = 0.25,
|
||||
BestFitSortField sortBy = BestFitSortField.Area)
|
||||
BestFitSortField sortBy = BestFitSortField.Area
|
||||
)
|
||||
{
|
||||
var strategies = BuildStrategies(drawing, spacing);
|
||||
|
||||
var candidateBags = new ConcurrentBag<List<PairCandidate>>();
|
||||
|
||||
Parallel.ForEach(strategies, strategy =>
|
||||
{
|
||||
candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize));
|
||||
});
|
||||
Parallel.ForEach(
|
||||
strategies,
|
||||
strategy =>
|
||||
{
|
||||
candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize));
|
||||
}
|
||||
);
|
||||
|
||||
var allCandidates = candidateBags.SelectMany(c => c).ToList();
|
||||
|
||||
Debug.WriteLine($"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates");
|
||||
Debug.WriteLine(
|
||||
$"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates"
|
||||
);
|
||||
|
||||
var results = _evaluator.EvaluateAll(allCandidates);
|
||||
|
||||
@@ -65,8 +77,12 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
|
||||
public List<TileResult> FindAndTile(
|
||||
Drawing drawing, Plate plate,
|
||||
double spacing = 0.25, double stepSize = 0.25, int topN = 10)
|
||||
Drawing drawing,
|
||||
Plate plate,
|
||||
double spacing = 0.25,
|
||||
double stepSize = 0.25,
|
||||
int topN = 10
|
||||
)
|
||||
{
|
||||
var bestFits = FindBestFits(drawing, spacing, stepSize);
|
||||
var tileEvaluator = new TileEvaluator();
|
||||
@@ -88,7 +104,10 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
foreach (var angle in angles)
|
||||
{
|
||||
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
||||
var desc = string.Format(
|
||||
"{0:F1} deg rotated, offset slide",
|
||||
Angle.ToDegrees(angle)
|
||||
);
|
||||
strategies.Add(new RotationSlideStrategy(angle, index++, desc, _distanceComputer));
|
||||
}
|
||||
|
||||
@@ -97,13 +116,7 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
private List<double> GetRotationAngles(Drawing drawing)
|
||||
{
|
||||
var angles = new List<double>
|
||||
{
|
||||
0,
|
||||
Angle.HalfPI,
|
||||
System.Math.PI,
|
||||
Angle.HalfPI * 3
|
||||
};
|
||||
var angles = new List<double> { 0, Angle.HalfPI, System.Math.PI, Angle.HalfPI * 3 };
|
||||
|
||||
var hullAngles = GetHullEdgeAngles(drawing);
|
||||
|
||||
@@ -119,7 +132,8 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
private List<double> GetHullEdgeAngles(Drawing drawing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
@@ -220,7 +234,10 @@ namespace OpenNest.Engine.BestFit
|
||||
angles.Add(angle);
|
||||
}
|
||||
|
||||
private List<BestFitResult> SortResults(List<BestFitResult> results, BestFitSortField sortBy)
|
||||
private List<BestFitResult> SortResults(
|
||||
List<BestFitResult> results,
|
||||
BestFitSortField sortBy
|
||||
)
|
||||
{
|
||||
switch (sortBy)
|
||||
{
|
||||
@@ -231,16 +248,19 @@ namespace OpenNest.Engine.BestFit
|
||||
case BestFitSortField.ShortestSide:
|
||||
return results.OrderBy(r => r.ShortestSide).ToList();
|
||||
case BestFitSortField.Type:
|
||||
return results.OrderBy(r => r.Candidate.StrategyIndex)
|
||||
.ThenBy(r => r.Candidate.TestNumber).ToList();
|
||||
return results
|
||||
.OrderBy(r => r.Candidate.StrategyIndex)
|
||||
.ThenBy(r => r.Candidate.TestNumber)
|
||||
.ToList();
|
||||
case BestFitSortField.OriginalSequence:
|
||||
return results.OrderBy(r => r.Candidate.TestNumber).ToList();
|
||||
case BestFitSortField.Keep:
|
||||
return results.OrderByDescending(r => r.Keep)
|
||||
.ThenBy(r => r.RotatedArea).ToList();
|
||||
return results
|
||||
.OrderByDescending(r => r.Keep)
|
||||
.ThenBy(r => r.RotatedArea)
|
||||
.ToList();
|
||||
case BestFitSortField.WhyKeepDrop:
|
||||
return results.OrderBy(r => r.Reason)
|
||||
.ThenBy(r => r.RotatedArea).ToList();
|
||||
return results.OrderBy(r => r.Reason).ThenBy(r => r.RotatedArea).ToList();
|
||||
default:
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -44,13 +44,17 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
if (!OptimalRotation.IsEqualTo(0))
|
||||
{
|
||||
var pairBounds = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var pairBounds = (
|
||||
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
|
||||
).GetBoundingBox();
|
||||
var center = pairBounds.Center;
|
||||
part1.Rotate(-OptimalRotation, center);
|
||||
part2.Rotate(-OptimalRotation, center);
|
||||
}
|
||||
|
||||
var finalBounds = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var finalBounds = (
|
||||
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
|
||||
).GetBoundingBox();
|
||||
var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom);
|
||||
part1.Offset(offset);
|
||||
part2.Offset(offset);
|
||||
@@ -106,7 +110,8 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var partEntities = ConvertProgram.ToGeometry(part.Program)
|
||||
var partEntities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
@@ -129,6 +134,6 @@ namespace OpenNest.Engine.BestFit
|
||||
Type,
|
||||
OriginalSequence,
|
||||
Keep,
|
||||
WhyKeepDrop
|
||||
WhyKeepDrop,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -10,7 +10,8 @@ namespace OpenNest.Engine.BestFit
|
||||
public double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets)
|
||||
SlideOffset[] offsets
|
||||
)
|
||||
{
|
||||
var count = offsets.Length;
|
||||
var results = new double[count];
|
||||
@@ -18,7 +19,8 @@ namespace OpenNest.Engine.BestFit
|
||||
var allMovingVerts = ExtractUniqueVertices(movingTemplateLines);
|
||||
var allStationaryVerts = ExtractUniqueVertices(stationaryLines);
|
||||
|
||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
var vertexCache =
|
||||
new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
@@ -26,69 +28,101 @@ namespace OpenNest.Engine.BestFit
|
||||
if (vertexCache.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true);
|
||||
var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false);
|
||||
var leading = FilterVerticesByProjection(
|
||||
allMovingVerts,
|
||||
offset.DirX,
|
||||
offset.DirY,
|
||||
keepHigh: true
|
||||
);
|
||||
var facing = FilterVerticesByProjection(
|
||||
allStationaryVerts,
|
||||
offset.DirX,
|
||||
offset.DirY,
|
||||
keepHigh: false
|
||||
);
|
||||
vertexCache[key] = (leading, facing);
|
||||
}
|
||||
|
||||
System.Threading.Tasks.Parallel.For(0, count, i =>
|
||||
{
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
System.Threading.Tasks.Parallel.For(
|
||||
0,
|
||||
count,
|
||||
i =>
|
||||
{
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
for (var j = 0; j < stationaryLines.Count; j++)
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
{
|
||||
var e = stationaryLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
vx, vy,
|
||||
e.StartPoint.X, e.StartPoint.Y,
|
||||
e.EndPoint.X, e.EndPoint.Y,
|
||||
dirX, dirY);
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
|
||||
if (d < minDist)
|
||||
for (var j = 0; j < stationaryLines.Count; j++)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
var e = stationaryLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
vx,
|
||||
vy,
|
||||
e.StartPoint.X,
|
||||
e.StartPoint.Y,
|
||||
e.EndPoint.X,
|
||||
e.EndPoint.Y,
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0)
|
||||
{
|
||||
results[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
for (var j = 0; j < movingTemplateLines.Count; j++)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var e = movingTemplateLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
svx, svy,
|
||||
e.StartPoint.X + offset.Dx, e.StartPoint.Y + offset.Dy,
|
||||
e.EndPoint.X + offset.Dx, e.EndPoint.Y + offset.Dy,
|
||||
oppX, oppY);
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
if (d < minDist)
|
||||
for (var j = 0; j < movingTemplateLines.Count; j++)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
var e = movingTemplateLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
svx,
|
||||
svy,
|
||||
e.StartPoint.X + offset.Dx,
|
||||
e.StartPoint.Y + offset.Dy,
|
||||
e.EndPoint.X + offset.Dx,
|
||||
e.EndPoint.Y + offset.Dy,
|
||||
oppX,
|
||||
oppY
|
||||
);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0)
|
||||
{
|
||||
results[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
results[i] = minDist;
|
||||
}
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -96,7 +130,8 @@ namespace OpenNest.Engine.BestFit
|
||||
public double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets)
|
||||
SlideOffset[] offsets
|
||||
)
|
||||
{
|
||||
var count = offsets.Length;
|
||||
var results = new double[count];
|
||||
@@ -107,7 +142,8 @@ namespace OpenNest.Engine.BestFit
|
||||
var movingCurves = ExtractCurveParams(movingEntities);
|
||||
var stationaryCurves = ExtractCurveParams(stationaryEntities);
|
||||
|
||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
var vertexCache =
|
||||
new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
@@ -115,106 +151,169 @@ namespace OpenNest.Engine.BestFit
|
||||
if (vertexCache.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true);
|
||||
var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false);
|
||||
var leading = FilterVerticesByProjection(
|
||||
allMovingVerts,
|
||||
offset.DirX,
|
||||
offset.DirY,
|
||||
keepHigh: true
|
||||
);
|
||||
var facing = FilterVerticesByProjection(
|
||||
allStationaryVerts,
|
||||
offset.DirX,
|
||||
offset.DirY,
|
||||
keepHigh: false
|
||||
);
|
||||
vertexCache[key] = (leading, facing);
|
||||
}
|
||||
|
||||
System.Threading.Tasks.Parallel.For(0, count, i =>
|
||||
{
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Leading moving vertices → stationary entities
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
System.Threading.Tasks.Parallel.For(
|
||||
0,
|
||||
count,
|
||||
i =>
|
||||
{
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Leading moving vertices → stationary entities
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
{
|
||||
var d = RayEntityDistance(vx, vy, stationaryEntities[j], 0, 0, dirX, dirY);
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
|
||||
if (d < minDist)
|
||||
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
var d = RayEntityDistance(
|
||||
vx,
|
||||
vy,
|
||||
stationaryEntities[j],
|
||||
0,
|
||||
0,
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
|
||||
// Case 2: Facing stationary vertices → moving entities (opposite direction)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
for (var j = 0; j < movingEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(svx, svy, movingEntities[j], offset.Dx, offset.Dy, oppX, oppY);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Curve-to-curve direct distance.
|
||||
// Vertex sampling misses the true contact between two curved entities
|
||||
// when the approach angle doesn't align with a sampled vertex.
|
||||
for (var m = 0; m < movingCurves.Length; m++)
|
||||
{
|
||||
var mc = movingCurves[m];
|
||||
var mcx = mc.Cx + offset.Dx;
|
||||
var mcy = mc.Cy + offset.Dy;
|
||||
|
||||
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||
{
|
||||
var sc = stationaryCurves[s];
|
||||
var d = SpatialQuery.RayCircleDistance(
|
||||
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
|
||||
|
||||
if (d >= minDist || d == double.MaxValue)
|
||||
continue;
|
||||
|
||||
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||
{
|
||||
var mx = mcx + d * dirX;
|
||||
var my = mcy + d * dirY;
|
||||
var toCx = sc.Cx - mx;
|
||||
var toCy = sc.Cy - my;
|
||||
|
||||
if (mc.Entity is Arc mArc)
|
||||
if (d < minDist)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sc.Entity is Arc sArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||
continue;
|
||||
minDist = d;
|
||||
if (d <= 0)
|
||||
{
|
||||
results[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
// Case 2: Facing stationary vertices → moving entities (opposite direction)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
for (var j = 0; j < movingEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(
|
||||
svx,
|
||||
svy,
|
||||
movingEntities[j],
|
||||
offset.Dx,
|
||||
offset.Dy,
|
||||
oppX,
|
||||
oppY
|
||||
);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0)
|
||||
{
|
||||
results[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Curve-to-curve direct distance.
|
||||
// Vertex sampling misses the true contact between two curved entities
|
||||
// when the approach angle doesn't align with a sampled vertex.
|
||||
for (var m = 0; m < movingCurves.Length; m++)
|
||||
{
|
||||
var mc = movingCurves[m];
|
||||
var mcx = mc.Cx + offset.Dx;
|
||||
var mcy = mc.Cy + offset.Dy;
|
||||
|
||||
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||
{
|
||||
var sc = stationaryCurves[s];
|
||||
var d = SpatialQuery.RayCircleDistance(
|
||||
mcx,
|
||||
mcy,
|
||||
sc.Cx,
|
||||
sc.Cy,
|
||||
mc.Radius + sc.Radius,
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
|
||||
if (d >= minDist || d == double.MaxValue)
|
||||
continue;
|
||||
|
||||
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||
{
|
||||
var mx = mcx + d * dirX;
|
||||
var my = mcy + d * dirY;
|
||||
var toCx = sc.Cx - mx;
|
||||
var toCy = sc.Cy - my;
|
||||
|
||||
if (mc.Entity is Arc mArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||
if (
|
||||
!Angle.IsBetweenRad(
|
||||
angle,
|
||||
mArc.StartAngle,
|
||||
mArc.EndAngle,
|
||||
mArc.IsReversed
|
||||
)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sc.Entity is Arc sArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||
if (
|
||||
!Angle.IsBetweenRad(
|
||||
angle,
|
||||
sArc.StartAngle,
|
||||
sArc.EndAngle,
|
||||
sArc.IsReversed
|
||||
)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
minDist = d;
|
||||
if (d <= 0)
|
||||
{
|
||||
results[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
}
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -222,7 +321,9 @@ namespace OpenNest.Engine.BestFit
|
||||
private readonly struct CurveParams
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
public readonly double Cx, Cy, Radius;
|
||||
public readonly double Cx,
|
||||
Cy,
|
||||
Radius;
|
||||
|
||||
public CurveParams(Entity entity, double cx, double cy, double radius)
|
||||
{
|
||||
@@ -239,7 +340,9 @@ namespace OpenNest.Engine.BestFit
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
if (entities[i] is Circle circle)
|
||||
curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius));
|
||||
curves.Add(
|
||||
new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius)
|
||||
);
|
||||
else if (entities[i] is Arc arc)
|
||||
curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius));
|
||||
}
|
||||
@@ -247,36 +350,56 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
|
||||
private static double RayEntityDistance(
|
||||
double vx, double vy, Entity entity,
|
||||
double entityOffsetX, double entityOffsetY,
|
||||
double dirX, double dirY)
|
||||
double vx,
|
||||
double vy,
|
||||
Entity entity,
|
||||
double entityOffsetX,
|
||||
double entityOffsetY,
|
||||
double dirX,
|
||||
double dirY
|
||||
)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
return SpatialQuery.RayEdgeDistance(
|
||||
vx, vy,
|
||||
line.StartPoint.X + entityOffsetX, line.StartPoint.Y + entityOffsetY,
|
||||
line.EndPoint.X + entityOffsetX, line.EndPoint.Y + entityOffsetY,
|
||||
dirX, dirY);
|
||||
vx,
|
||||
vy,
|
||||
line.StartPoint.X + entityOffsetX,
|
||||
line.StartPoint.Y + entityOffsetY,
|
||||
line.EndPoint.X + entityOffsetX,
|
||||
line.EndPoint.Y + entityOffsetY,
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
}
|
||||
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
return SpatialQuery.RayArcDistance(
|
||||
vx, vy,
|
||||
arc.Center.X + entityOffsetX, arc.Center.Y + entityOffsetY,
|
||||
vx,
|
||||
vy,
|
||||
arc.Center.X + entityOffsetX,
|
||||
arc.Center.Y + entityOffsetY,
|
||||
arc.Radius,
|
||||
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
||||
dirX, dirY);
|
||||
arc.StartAngle,
|
||||
arc.EndAngle,
|
||||
arc.IsReversed,
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
}
|
||||
|
||||
if (entity is Circle circle)
|
||||
{
|
||||
return SpatialQuery.RayCircleDistance(
|
||||
vx, vy,
|
||||
circle.Center.X + entityOffsetX, circle.Center.Y + entityOffsetY,
|
||||
vx,
|
||||
vy,
|
||||
circle.Center.X + entityOffsetX,
|
||||
circle.Center.Y + entityOffsetY,
|
||||
circle.Radius,
|
||||
dirX, dirY);
|
||||
dirX,
|
||||
dirY
|
||||
);
|
||||
}
|
||||
|
||||
return double.MaxValue;
|
||||
@@ -352,7 +475,11 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
|
||||
private static Vector[] FilterVerticesByProjection(
|
||||
Vector[] vertices, double dirX, double dirY, bool keepHigh)
|
||||
Vector[] vertices,
|
||||
double dirX,
|
||||
double dirY,
|
||||
bool keepHigh
|
||||
)
|
||||
{
|
||||
if (vertices.Length == 0)
|
||||
return vertices;
|
||||
@@ -364,8 +491,10 @@ namespace OpenNest.Engine.BestFit
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
projections[i] = vertices[i].X * dirX + vertices[i].Y * dirY;
|
||||
if (projections[i] < min) min = projections[i];
|
||||
if (projections[i] > max) max = projections[i];
|
||||
if (projections[i] < min)
|
||||
min = projections[i];
|
||||
if (projections[i] > max)
|
||||
max = projections[i];
|
||||
}
|
||||
|
||||
var midpoint = (min + max) / 2;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -15,7 +15,8 @@ namespace OpenNest.Engine.BestFit
|
||||
public double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets)
|
||||
SlideOffset[] offsets
|
||||
)
|
||||
{
|
||||
var stationarySegments = SpatialQuery.FlattenLines(stationaryLines);
|
||||
var movingSegments = SpatialQuery.FlattenLines(movingTemplateLines);
|
||||
@@ -31,15 +32,21 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
|
||||
return _slideComputer.ComputeBatchMultiDir(
|
||||
stationarySegments, stationaryLines.Count,
|
||||
movingSegments, movingTemplateLines.Count,
|
||||
flatOffsets, count, directions);
|
||||
stationarySegments,
|
||||
stationaryLines.Count,
|
||||
movingSegments,
|
||||
movingTemplateLines.Count,
|
||||
flatOffsets,
|
||||
count,
|
||||
directions
|
||||
);
|
||||
}
|
||||
|
||||
public double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets)
|
||||
SlideOffset[] offsets
|
||||
)
|
||||
{
|
||||
// GPU path doesn't support native entities yet — fall back to CPU.
|
||||
var cpu = new CpuDistanceComputer();
|
||||
@@ -52,9 +59,12 @@ namespace OpenNest.Engine.BestFit
|
||||
/// </summary>
|
||||
private static int DirectionVectorToInt(double dirX, double dirY)
|
||||
{
|
||||
if (dirX < -0.5) return (int)PushDirection.Left;
|
||||
if (dirX > 0.5) return (int)PushDirection.Right;
|
||||
if (dirY < -0.5) return (int)PushDirection.Down;
|
||||
if (dirX < -0.5)
|
||||
return (int)PushDirection.Left;
|
||||
if (dirX > 0.5)
|
||||
return (int)PushDirection.Right;
|
||||
if (dirY < -0.5)
|
||||
return (int)PushDirection.Down;
|
||||
return (int)PushDirection.Up;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -8,11 +8,13 @@ namespace OpenNest.Engine.BestFit
|
||||
double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets);
|
||||
SlideOffset[] offsets
|
||||
);
|
||||
|
||||
double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets);
|
||||
SlideOffset[] offsets
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,27 @@ namespace OpenNest.Engine.BestFit
|
||||
/// <param name="direction">Push direction.</param>
|
||||
/// <returns>Array of minimum distances, one per offset position.</returns>
|
||||
double[] ComputeBatch(
|
||||
double[] stationarySegments, int stationaryCount,
|
||||
double[] movingTemplateSegments, int movingCount,
|
||||
double[] offsets, int offsetCount,
|
||||
PushDirection direction);
|
||||
double[] stationarySegments,
|
||||
int stationaryCount,
|
||||
double[] movingTemplateSegments,
|
||||
int movingCount,
|
||||
double[] offsets,
|
||||
int offsetCount,
|
||||
PushDirection direction
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Computes minimum directional distance for offsets with per-offset directions.
|
||||
/// Uploads segment data once for all offsets, reducing GPU round-trips.
|
||||
/// </summary>
|
||||
double[] ComputeBatchMultiDir(
|
||||
double[] stationarySegments, int stationaryCount,
|
||||
double[] movingTemplateSegments, int movingCount,
|
||||
double[] offsets, int offsetCount,
|
||||
int[] directions);
|
||||
double[] stationarySegments,
|
||||
int stationaryCount,
|
||||
double[] movingTemplateSegments,
|
||||
int movingCount,
|
||||
double[] offsets,
|
||||
int offsetCount,
|
||||
int[] directions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -10,8 +10,14 @@ namespace OpenNest.Engine.BestFit
|
||||
private readonly Polygon _stationaryHull;
|
||||
private readonly Vector _correction;
|
||||
|
||||
public NfpSlideStrategy(double part2Rotation, int type, string description,
|
||||
Polygon stationaryPerimeter, Polygon stationaryHull, Vector correction)
|
||||
public NfpSlideStrategy(
|
||||
double part2Rotation,
|
||||
int type,
|
||||
string description,
|
||||
Polygon stationaryPerimeter,
|
||||
Polygon stationaryHull,
|
||||
Vector correction
|
||||
)
|
||||
{
|
||||
_part2Rotation = part2Rotation;
|
||||
StrategyIndex = type;
|
||||
@@ -28,8 +34,13 @@ namespace OpenNest.Engine.BestFit
|
||||
/// Creates an NfpSlideStrategy by extracting polygon data from a drawing.
|
||||
/// Returns null if the drawing has no valid perimeter.
|
||||
/// </summary>
|
||||
public static NfpSlideStrategy Create(Drawing drawing, double part2Rotation,
|
||||
int type, string description, double spacing)
|
||||
public static NfpSlideStrategy Create(
|
||||
Drawing drawing,
|
||||
double part2Rotation,
|
||||
int type,
|
||||
string description,
|
||||
double spacing
|
||||
)
|
||||
{
|
||||
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, spacing / 2);
|
||||
|
||||
@@ -38,18 +49,32 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var hull = ConvexHull.Compute(result.Polygon.Vertices);
|
||||
|
||||
return new NfpSlideStrategy(part2Rotation, type, description,
|
||||
result.Polygon, hull, result.Correction);
|
||||
return new NfpSlideStrategy(
|
||||
part2Rotation,
|
||||
type,
|
||||
description,
|
||||
result.Polygon,
|
||||
hull,
|
||||
result.Correction
|
||||
);
|
||||
}
|
||||
|
||||
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
||||
public List<PairCandidate> GenerateCandidates(
|
||||
Drawing drawing,
|
||||
double spacing,
|
||||
double stepSize
|
||||
)
|
||||
{
|
||||
var candidates = new List<PairCandidate>();
|
||||
|
||||
if (stepSize <= 0)
|
||||
return candidates;
|
||||
|
||||
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
|
||||
var orbitingPerimeter = PolygonHelper.RotatePolygon(
|
||||
_stationaryPerimeter,
|
||||
_part2Rotation,
|
||||
reNormalize: true
|
||||
);
|
||||
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
|
||||
|
||||
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
|
||||
@@ -79,9 +104,7 @@ namespace OpenNest.Engine.BestFit
|
||||
for (var s = 1; s < steps; s++)
|
||||
{
|
||||
var t = (double)s / steps;
|
||||
var sample = new Vector(
|
||||
verts[i].X + dx * t,
|
||||
verts[i].Y + dy * t);
|
||||
var sample = new Vector(verts[i].X + dx * t, verts[i].Y + dy * t);
|
||||
var sampleOffset = ApplyCorrection(sample, _correction);
|
||||
candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++));
|
||||
}
|
||||
@@ -96,7 +119,12 @@ namespace OpenNest.Engine.BestFit
|
||||
return new Vector(nfpVertex.X - correction.X, nfpVertex.Y - correction.Y);
|
||||
}
|
||||
|
||||
private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber)
|
||||
private PairCandidate MakeCandidate(
|
||||
Drawing drawing,
|
||||
Vector offset,
|
||||
double spacing,
|
||||
int testNumber
|
||||
)
|
||||
{
|
||||
return new PairCandidate
|
||||
{
|
||||
@@ -106,7 +134,7 @@ namespace OpenNest.Engine.BestFit
|
||||
Part2Offset = offset,
|
||||
StrategyIndex = StrategyIndex,
|
||||
TestNumber = testNumber,
|
||||
Spacing = spacing
|
||||
Spacing = spacing,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -24,10 +24,13 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var resultBag = new ConcurrentBag<BestFitResult>();
|
||||
|
||||
Parallel.ForEach(candidates, c =>
|
||||
{
|
||||
resultBag.Add(Evaluate(c, perimeterDrawing));
|
||||
});
|
||||
Parallel.ForEach(
|
||||
candidates,
|
||||
c =>
|
||||
{
|
||||
resultBag.Add(Evaluate(c, perimeterDrawing));
|
||||
}
|
||||
);
|
||||
|
||||
return resultBag.ToList();
|
||||
}
|
||||
@@ -56,7 +59,10 @@ namespace OpenNest.Engine.BestFit
|
||||
allPoints.AddRange(GetPartVertices(part2));
|
||||
|
||||
// Find optimal bounding rectangle via rotating calipers
|
||||
double bestArea, bestWidth, bestHeight, bestRotation;
|
||||
double bestArea,
|
||||
bestWidth,
|
||||
bestHeight,
|
||||
bestRotation;
|
||||
List<double> hullAngles = null;
|
||||
|
||||
if (allPoints.Count >= 3)
|
||||
@@ -71,7 +77,9 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
else
|
||||
{
|
||||
var combinedBox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var combinedBox = (
|
||||
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
|
||||
).GetBoundingBox();
|
||||
bestArea = combinedBox.Area();
|
||||
bestWidth = combinedBox.Width;
|
||||
bestHeight = combinedBox.Length;
|
||||
@@ -100,14 +108,16 @@ namespace OpenNest.Engine.BestFit
|
||||
TrueArea = trueArea,
|
||||
HullAngles = hullAngles,
|
||||
Keep = !overlaps,
|
||||
Reason = overlaps ? "Overlap detected" : "Valid"
|
||||
Reason = overlaps ? "Overlap detected" : "Valid",
|
||||
};
|
||||
}
|
||||
|
||||
private static Drawing CreatePerimeterDrawing(Drawing source)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(source.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(source.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
var profile = new ShapeProfile(entities);
|
||||
var program = ConvertGeometry.ToProgram(profile.Perimeter);
|
||||
return new Drawing(source.Name, program);
|
||||
@@ -115,18 +125,23 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
private static Shape GetPerimeterShape(Part part)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
if (shapes.Count == 0) return null;
|
||||
if (shapes.Count == 0)
|
||||
return null;
|
||||
shapes[0].Offset(part.Location);
|
||||
return shapes[0];
|
||||
}
|
||||
|
||||
private static List<Vector> GetPartVertices(Part part)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
var points = new List<Vector>();
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public static class PolygonHelper
|
||||
{
|
||||
public static PolygonExtractionResult ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
||||
public static PolygonExtractionResult ExtractPerimeterPolygon(
|
||||
Drawing drawing,
|
||||
double halfSpacing
|
||||
)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
@@ -25,9 +29,8 @@ namespace OpenNest.Engine.BestFit
|
||||
// Ensure CW winding for correct outward offset direction.
|
||||
definedShape.NormalizeWinding();
|
||||
|
||||
var inflated = halfSpacing > 0
|
||||
? (perimeter.OffsetOutward(halfSpacing) ?? perimeter)
|
||||
: perimeter;
|
||||
var inflated =
|
||||
halfSpacing > 0 ? (perimeter.OffsetOutward(halfSpacing) ?? perimeter) : perimeter;
|
||||
|
||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||
var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
|
||||
@@ -57,9 +60,7 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
foreach (var v in polygon.Vertices)
|
||||
{
|
||||
result.Vertices.Add(new Vector(
|
||||
v.X * cos - v.Y * sin,
|
||||
v.X * sin + v.Y * cos));
|
||||
result.Vertices.Add(new Vector(v.X * cos - v.Y * sin, v.X * sin + v.Y * cos));
|
||||
}
|
||||
|
||||
if (reNormalize)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -9,14 +9,18 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
private static readonly (double DirX, double DirY)[] PushDirections =
|
||||
{
|
||||
(-1, 0), // Left
|
||||
(0, -1), // Down
|
||||
(1, 0), // Right
|
||||
(0, 1) // Up
|
||||
(-1, 0), // Left
|
||||
(0, -1), // Down
|
||||
(1, 0), // Right
|
||||
(0, 1), // Up
|
||||
};
|
||||
|
||||
public RotationSlideStrategy(double part2Rotation, int strategyIndex, string description,
|
||||
IDistanceComputer distanceComputer)
|
||||
public RotationSlideStrategy(
|
||||
double part2Rotation,
|
||||
int strategyIndex,
|
||||
string description,
|
||||
IDistanceComputer distanceComputer
|
||||
)
|
||||
{
|
||||
Part2Rotation = part2Rotation;
|
||||
StrategyIndex = strategyIndex;
|
||||
@@ -28,7 +32,11 @@ namespace OpenNest.Engine.BestFit
|
||||
public int StrategyIndex { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
||||
public List<PairCandidate> GenerateCandidates(
|
||||
Drawing drawing,
|
||||
double spacing,
|
||||
double stepSize
|
||||
)
|
||||
{
|
||||
var candidates = new List<PairCandidate>();
|
||||
|
||||
@@ -48,7 +56,10 @@ namespace OpenNest.Engine.BestFit
|
||||
return candidates;
|
||||
|
||||
var distances = _distanceComputer.ComputeDistances(
|
||||
part1Entities, part2Entities, offsets);
|
||||
part1Entities,
|
||||
part2Entities,
|
||||
offsets
|
||||
);
|
||||
|
||||
var testNumber = 0;
|
||||
|
||||
@@ -60,24 +71,32 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var finalPosition = new Vector(
|
||||
part2Template.Location.X + offsets[i].Dx + offsets[i].DirX * slideDist,
|
||||
part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist);
|
||||
part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist
|
||||
);
|
||||
|
||||
candidates.Add(new PairCandidate
|
||||
{
|
||||
Drawing = drawing,
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = Part2Rotation,
|
||||
Part2Offset = finalPosition,
|
||||
StrategyIndex = StrategyIndex,
|
||||
TestNumber = testNumber++,
|
||||
Spacing = spacing
|
||||
});
|
||||
candidates.Add(
|
||||
new PairCandidate
|
||||
{
|
||||
Drawing = drawing,
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = Part2Rotation,
|
||||
Part2Offset = finalPosition,
|
||||
StrategyIndex = StrategyIndex,
|
||||
TestNumber = testNumber++,
|
||||
Spacing = spacing,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static SlideOffset[] BuildOffsets(Box bbox1, Box bbox2, double spacing, double stepSize)
|
||||
private static SlideOffset[] BuildOffsets(
|
||||
Box bbox1,
|
||||
Box bbox2,
|
||||
double spacing,
|
||||
double stepSize
|
||||
)
|
||||
{
|
||||
var offsets = new List<SlideOffset>();
|
||||
|
||||
@@ -85,7 +104,9 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
var isHorizontalPush = System.Math.Abs(dirX) > System.Math.Abs(dirY);
|
||||
|
||||
double perpMin, perpMax, pushStartOffset;
|
||||
double perpMin,
|
||||
perpMax,
|
||||
pushStartOffset;
|
||||
|
||||
if (isHorizontalPush)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
@@ -16,7 +16,12 @@ namespace OpenNest.Engine.BestFit.Tiling
|
||||
return result1.PartsNested >= result2.PartsNested ? result1 : result2;
|
||||
}
|
||||
|
||||
private TileResult TryTile(BestFitResult bestFit, double plateWidth, double plateHeight, bool rotatePair)
|
||||
private TileResult TryTile(
|
||||
BestFitResult bestFit,
|
||||
double plateWidth,
|
||||
double plateHeight,
|
||||
bool rotatePair
|
||||
)
|
||||
{
|
||||
var pairWidth = rotatePair ? bestFit.BoundingHeight : bestFit.BoundingWidth;
|
||||
var pairHeight = rotatePair ? bestFit.BoundingWidth : bestFit.BoundingHeight;
|
||||
@@ -36,13 +41,16 @@ namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
for (var col = 0; col < cols; col++)
|
||||
{
|
||||
placements.Add(new PairPlacement
|
||||
{
|
||||
Position = new Vector(
|
||||
col * (pairWidth + spacing),
|
||||
row * (pairHeight + spacing)),
|
||||
PairRotation = rotatePair ? Angle.HalfPI : 0
|
||||
});
|
||||
placements.Add(
|
||||
new PairPlacement
|
||||
{
|
||||
Position = new Vector(
|
||||
col * (pairWidth + spacing),
|
||||
row * (pairHeight + spacing)
|
||||
),
|
||||
PairRotation = rotatePair ? Angle.HalfPI : 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +63,7 @@ namespace OpenNest.Engine.BestFit.Tiling
|
||||
Columns = cols,
|
||||
Utilization = plateArea > 0 ? usedArea / plateArea : 0,
|
||||
Placements = placements,
|
||||
PairRotated = rotatePair
|
||||
PairRotated = rotatePair,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
@@ -23,8 +23,8 @@ namespace OpenNest.Engine
|
||||
var angle = drawing.Source?.Angle ?? 0.0;
|
||||
|
||||
// Clone program (never mutate the source).
|
||||
var pgm = (drawing.Program.Clone() as OpenNest.CNC.Program)
|
||||
?? new OpenNest.CNC.Program();
|
||||
var pgm =
|
||||
(drawing.Program.Clone() as OpenNest.CNC.Program) ?? new OpenNest.CNC.Program();
|
||||
|
||||
if (!Tolerance.IsEqualTo(angle, 0))
|
||||
pgm.Rotate(angle, pgm.BoundingBox().Center);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
@@ -24,7 +24,7 @@ namespace OpenNest.CirclePacking
|
||||
{
|
||||
Location = this.Location,
|
||||
Size = this.Size,
|
||||
Items = new List<Item>(Items)
|
||||
Items = new List<Item>(Items),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
internal class FillEndEven : FillEngine
|
||||
{
|
||||
public FillEndEven(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public override void Fill(Item item)
|
||||
{
|
||||
var max = new Vector(
|
||||
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
||||
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
||||
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon
|
||||
);
|
||||
|
||||
var rows = System.Math.Floor((Bin.Length + Tolerance.Epsilon) / (item.Diameter));
|
||||
|
||||
@@ -36,10 +35,7 @@ namespace OpenNest.CirclePacking
|
||||
|
||||
for (; y <= max.Y; y += yoffset)
|
||||
{
|
||||
Bin.Items.Add(new Item
|
||||
{
|
||||
Center = new Vector(x, y)
|
||||
});
|
||||
Bin.Items.Add(new Item { Center = new Vector(x, y) });
|
||||
}
|
||||
|
||||
column++;
|
||||
@@ -62,10 +58,7 @@ namespace OpenNest.CirclePacking
|
||||
|
||||
for (; y <= max.Y; y += yoffset)
|
||||
{
|
||||
Bin.Items.Add(new Item
|
||||
{
|
||||
Center = new Vector(x, y)
|
||||
});
|
||||
Bin.Items.Add(new Item { Center = new Vector(x, y) });
|
||||
}
|
||||
|
||||
column++;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
internal class FillEndOdd : FillEngine
|
||||
{
|
||||
public FillEndOdd(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public override void Fill(Item item)
|
||||
{
|
||||
@@ -37,7 +35,8 @@ namespace OpenNest.CirclePacking
|
||||
|
||||
var max = new Vector(
|
||||
bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
||||
bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
||||
bin.Top - item.BoundingBox.Top + Tolerance.Epsilon
|
||||
);
|
||||
|
||||
var primarySize = horizontal ? bin.Width : bin.Length;
|
||||
var count = System.Math.Floor((primarySize + Tolerance.Epsilon) / item.Diameter);
|
||||
@@ -64,7 +63,9 @@ namespace OpenNest.CirclePacking
|
||||
for (; inner <= innerMax; inner += primaryOffset)
|
||||
{
|
||||
var addedItem = item.Clone() as Item;
|
||||
addedItem.Center = horizontal ? new Vector(inner, outer) : new Vector(outer, inner);
|
||||
addedItem.Center = horizontal
|
||||
? new Vector(inner, outer)
|
||||
: new Vector(outer, inner);
|
||||
bin.Items.Add(addedItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
internal abstract class FillEngine
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace OpenNest.CirclePacking
|
||||
{
|
||||
Radius = this.Radius,
|
||||
Center = this.Center,
|
||||
Id = this.Id
|
||||
Id = this.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
@@ -5,21 +10,18 @@ using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public class DefaultNestEngine : NestEngineBase
|
||||
{
|
||||
public DefaultNestEngine(Plate plate) : base(plate) { }
|
||||
public DefaultNestEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Default";
|
||||
|
||||
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)";
|
||||
public override string Description =>
|
||||
"Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)";
|
||||
|
||||
private readonly AngleCandidateBuilder angleBuilder = new();
|
||||
|
||||
@@ -29,7 +31,11 @@ namespace OpenNest
|
||||
set => angleBuilder.ForceFullSweep = value;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
)
|
||||
{
|
||||
return angleBuilder.Build(item, classification, workArea);
|
||||
}
|
||||
@@ -41,8 +47,12 @@ namespace OpenNest
|
||||
|
||||
// --- Public Fill API ---
|
||||
|
||||
public override List<Part> Fill(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public override List<Part> Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
PhaseResults.Clear();
|
||||
AngleResults.Clear();
|
||||
@@ -67,18 +77,23 @@ namespace OpenNest
|
||||
var fast = TryFillSmallQuantity(canonicalItem, workArea);
|
||||
if (fast != null && fast.Count >= canonicalItem.Quantity)
|
||||
{
|
||||
Debug.WriteLine($"[Fill] Fast path: placed {fast.Count} parts for qty={canonicalItem.Quantity}");
|
||||
Debug.WriteLine(
|
||||
$"[Fill] Fast path: placed {fast.Count} parts for qty={canonicalItem.Quantity}"
|
||||
);
|
||||
WinnerPhase = NestPhase.Pairs;
|
||||
fast = RebindAndUnCanonicalize(fast, originalDrawing, sourceAngle);
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = fast,
|
||||
WorkArea = workArea,
|
||||
Description = $"Fast path: {fast.Count} parts",
|
||||
IsOverallBest = true,
|
||||
});
|
||||
ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = fast,
|
||||
WorkArea = workArea,
|
||||
Description = $"Fast path: {fast.Count} parts",
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
return fast;
|
||||
}
|
||||
}
|
||||
@@ -88,16 +103,24 @@ namespace OpenNest
|
||||
{
|
||||
effectiveWorkArea = ShrinkWorkArea(canonicalItem, workArea, Plate.PartSpacing);
|
||||
if (effectiveWorkArea != workArea)
|
||||
Debug.WriteLine($"[Fill] Low-qty shrink: {canonicalItem.Quantity} requested, " +
|
||||
$"from {workArea.Width:F1}x{workArea.Length:F1} " +
|
||||
$"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}");
|
||||
Debug.WriteLine(
|
||||
$"[Fill] Low-qty shrink: {canonicalItem.Quantity} requested, "
|
||||
+ $"from {workArea.Width:F1}x{workArea.Length:F1} "
|
||||
+ $"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}"
|
||||
);
|
||||
}
|
||||
|
||||
var best = RunFillPipeline(canonicalItem, effectiveWorkArea, progress, token);
|
||||
|
||||
if (canonicalItem.Quantity > 0 && best.Count < canonicalItem.Quantity && effectiveWorkArea != workArea)
|
||||
if (
|
||||
canonicalItem.Quantity > 0
|
||||
&& best.Count < canonicalItem.Quantity
|
||||
&& effectiveWorkArea != workArea
|
||||
)
|
||||
{
|
||||
Debug.WriteLine($"[Fill] Low-qty fallback: got {best.Count}, need {canonicalItem.Quantity}, retrying full area");
|
||||
Debug.WriteLine(
|
||||
$"[Fill] Low-qty fallback: got {best.Count}, need {canonicalItem.Quantity}, retrying full area"
|
||||
);
|
||||
PhaseResults.Clear();
|
||||
AngleResults.Clear();
|
||||
best = RunFillPipeline(canonicalItem, workArea, progress, token);
|
||||
@@ -108,15 +131,18 @@ namespace OpenNest
|
||||
|
||||
best = RebindAndUnCanonicalize(best, originalDrawing, sourceAngle);
|
||||
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
|
||||
return best;
|
||||
}
|
||||
@@ -126,7 +152,11 @@ namespace OpenNest
|
||||
/// original Drawing (so consumers see the user's drawing identity, not the transient canonical copy)
|
||||
/// and composes sourceAngle onto each Part's rotation via CanonicalFrame.FromCanonical.
|
||||
/// </summary>
|
||||
private static List<Part> RebindAndUnCanonicalize(List<Part> parts, Drawing original, double sourceAngle)
|
||||
private static List<Part> RebindAndUnCanonicalize(
|
||||
List<Part> parts,
|
||||
Drawing original,
|
||||
double sourceAngle
|
||||
)
|
||||
{
|
||||
if (parts == null || parts.Count == 0)
|
||||
return parts;
|
||||
@@ -164,8 +194,10 @@ namespace OpenNest
|
||||
private static List<Part> TryPlaceSingle(Drawing drawing, Box workArea)
|
||||
{
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
if (part.BoundingBox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
part.BoundingBox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
part.BoundingBox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| part.BoundingBox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return null;
|
||||
|
||||
part.Offset(workArea.Location - part.BoundingBox.Location);
|
||||
@@ -175,7 +207,11 @@ namespace OpenNest
|
||||
private List<Part> TryPlaceBestFitPair(Drawing drawing, Box workArea)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
drawing,
|
||||
Plate.Size.Length,
|
||||
Plate.Size.Width,
|
||||
Plate.PartSpacing
|
||||
);
|
||||
|
||||
// Build pair candidates with a canonical drawing so their geometry matches
|
||||
// the coordinate frame of the cached fit results.
|
||||
@@ -189,9 +225,15 @@ namespace OpenNest
|
||||
continue;
|
||||
|
||||
// Skip pairs that can't possibly fit the work area in either orientation.
|
||||
if (fit.ShortestSide > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
||||
if (
|
||||
fit.ShortestSide
|
||||
> System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon
|
||||
)
|
||||
continue;
|
||||
if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
||||
if (
|
||||
fit.LongestSide
|
||||
> System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon
|
||||
)
|
||||
continue;
|
||||
|
||||
var landscape = fit.BuildParts(canonicalDrawing);
|
||||
@@ -247,8 +289,10 @@ namespace OpenNest
|
||||
private static bool TryOffsetToWorkArea(List<Part> parts, Box workArea)
|
||||
{
|
||||
var bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||
if (bbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
bbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
bbox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| bbox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return false;
|
||||
|
||||
var offset = workArea.Location - bbox.Location;
|
||||
@@ -271,7 +315,10 @@ namespace OpenNest
|
||||
return workArea;
|
||||
|
||||
var bin = new Bin { Size = new Size(workArea.Width, workArea.Length) };
|
||||
var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) };
|
||||
var packItem = new Item
|
||||
{
|
||||
Size = new Size(bbox.Width + spacing, bbox.Length + spacing),
|
||||
};
|
||||
var packer = new FillBestFit(bin);
|
||||
packer.Fill(packItem);
|
||||
var fullCount = bin.Items.Count;
|
||||
@@ -303,8 +350,12 @@ namespace OpenNest
|
||||
return new Box(workArea.X, workArea.Y, newLength, newWidth);
|
||||
}
|
||||
|
||||
private List<Part> RunFillPipeline(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
private List<Part> RunFillPipeline(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var context = new FillContext
|
||||
{
|
||||
@@ -326,8 +377,12 @@ namespace OpenNest
|
||||
return context.CurrentBest ?? new List<Part>();
|
||||
}
|
||||
|
||||
public override List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public override List<Part> Fill(
|
||||
List<Part> groupParts,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
if (groupParts == null || groupParts.Count == 0)
|
||||
return new List<Part>();
|
||||
@@ -346,25 +401,34 @@ namespace OpenNest
|
||||
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer);
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||
Debug.WriteLine(
|
||||
$"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"
|
||||
);
|
||||
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Linear,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Linear,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
// --- Pack API ---
|
||||
|
||||
public override List<Part> PackArea(Box box, List<NestItem> items,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public override List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
|
||||
var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
|
||||
@@ -399,7 +463,10 @@ namespace OpenNest
|
||||
sw.Stop();
|
||||
|
||||
var phaseResult = new PhaseResult(
|
||||
strategy.Phase, result?.Count ?? 0, sw.ElapsedMilliseconds);
|
||||
strategy.Phase,
|
||||
result?.Count ?? 0,
|
||||
sw.ElapsedMilliseconds
|
||||
);
|
||||
context.PhaseResults.Add(phaseResult);
|
||||
|
||||
// Keep engine's PhaseResults in sync so BuildProgressSummary() works
|
||||
@@ -409,7 +476,11 @@ namespace OpenNest
|
||||
// FillContext.ReportProgress updates CurrentBest during the
|
||||
// strategy's angle sweep. This catches strategies that return a
|
||||
// result without reporting it (e.g. RectBestFit).
|
||||
var improved = context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea);
|
||||
var improved = context.Policy.Comparer.IsBetter(
|
||||
result,
|
||||
context.CurrentBest,
|
||||
context.WorkArea
|
||||
);
|
||||
if (improved)
|
||||
{
|
||||
context.CurrentBest = result;
|
||||
@@ -419,15 +490,18 @@ namespace OpenNest
|
||||
|
||||
if (improved && context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
{
|
||||
ReportProgress(context.Progress, new ProgressReport
|
||||
{
|
||||
Phase = context.WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = context.CurrentBest,
|
||||
WorkArea = context.WorkArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
ReportProgress(
|
||||
context.Progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = context.WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = context.CurrentBest,
|
||||
WorkArea = context.WorkArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,6 +512,5 @@ namespace OpenNest
|
||||
|
||||
RecordProductiveAngles(context.AngleResults);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -25,7 +25,11 @@ namespace OpenNest.Engine.Fill
|
||||
return new List<double> { 0 };
|
||||
|
||||
case PartType.Rectangle:
|
||||
return new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
return new List<double>
|
||||
{
|
||||
classification.PrimaryAngle,
|
||||
classification.PrimaryAngle + Angle.HalfPI,
|
||||
};
|
||||
|
||||
default:
|
||||
return BuildIrregularAngles(item, classification.PrimaryAngle, workArea);
|
||||
@@ -84,7 +88,11 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static List<double> ApplyMlPrediction(
|
||||
NestItem item, Box workArea, double[] baseAngles, List<double> fallback)
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
double[] baseAngles,
|
||||
List<double> fallback
|
||||
)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features == null)
|
||||
@@ -108,7 +116,9 @@ namespace OpenNest.Engine.Fill
|
||||
mlAngles.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total");
|
||||
Debug.WriteLine(
|
||||
$"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total"
|
||||
);
|
||||
return mlAngles;
|
||||
}
|
||||
|
||||
@@ -121,7 +131,9 @@ namespace OpenNest.Engine.Fill
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
|
||||
Debug.WriteLine(
|
||||
$"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)"
|
||||
);
|
||||
return pruned;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ namespace OpenNest
|
||||
|
||||
internal static class BestCombination
|
||||
{
|
||||
public static CombinationResult FindFrom2(double length1, double length2, double overallLength)
|
||||
public static CombinationResult FindFrom2(
|
||||
double length1,
|
||||
double length2,
|
||||
double overallLength
|
||||
)
|
||||
{
|
||||
overallLength += Tolerance.Epsilon;
|
||||
var count1 = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
@@ -14,8 +14,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
@@ -26,8 +26,8 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
@@ -37,8 +37,13 @@ namespace OpenNest.Engine.Fill
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, Vector direction)
|
||||
public static double Push(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
Vector direction
|
||||
)
|
||||
{
|
||||
var opposite = -direction;
|
||||
|
||||
@@ -84,33 +89,59 @@ namespace OpenNest.Engine.Fill
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var obstacleSpacingBox = obstacleSpacingBoxes[i];
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, opposite);
|
||||
var reverseGap = SpatialQuery.DirectionalGap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
opposite
|
||||
);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, direction);
|
||||
var gap = SpatialQuery.DirectionalGap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
direction
|
||||
);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingSpacingBox, obstacleSpacingBox, direction))
|
||||
if (
|
||||
!SpatialQuery.PerpendicularOverlap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
direction
|
||||
)
|
||||
)
|
||||
continue;
|
||||
|
||||
movingEntities ??= halfSpacing > 0
|
||||
? (needCutouts
|
||||
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
|
||||
: (needCutouts
|
||||
? PartGeometry.GetPartEntities(moving)
|
||||
: PartGeometry.GetPerimeterEntities(moving));
|
||||
movingEntities ??=
|
||||
halfSpacing > 0
|
||||
? (
|
||||
needCutouts
|
||||
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing)
|
||||
)
|
||||
: (
|
||||
needCutouts
|
||||
? PartGeometry.GetPartEntities(moving)
|
||||
: PartGeometry.GetPerimeterEntities(moving)
|
||||
);
|
||||
|
||||
obstacleEntities[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||
obstacleEntities[i] ??=
|
||||
halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
||||
if (d <= Tolerance.Epsilon
|
||||
var d = SpatialQuery.DirectionalDistance(
|
||||
movingEntities,
|
||||
obstacleEntities[i],
|
||||
direction
|
||||
);
|
||||
if (
|
||||
d <= Tolerance.Epsilon
|
||||
&& partSpacing <= Tolerance.Epsilon
|
||||
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction))
|
||||
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -133,8 +164,12 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static Box SpacingBounds(Box box, double spacing)
|
||||
{
|
||||
return new Box(box.Left - spacing, box.Bottom - spacing,
|
||||
box.Length + 2 * spacing, box.Width + 2 * spacing);
|
||||
return new Box(
|
||||
box.Left - spacing,
|
||||
box.Bottom - spacing,
|
||||
box.Length + 2 * spacing,
|
||||
box.Width + 2 * spacing
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
||||
@@ -162,8 +197,13 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
public static double Push(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
|
||||
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
|
||||
@@ -174,17 +214,32 @@ namespace OpenNest.Engine.Fill
|
||||
/// Much faster but less precise — use as a coarse positioning pass before
|
||||
/// a full geometry Push.
|
||||
/// </summary>
|
||||
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
public static double PushBoundingBox(
|
||||
List<Part> movingParts,
|
||||
Plate plate,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
return PushBoundingBox(
|
||||
movingParts,
|
||||
obstacleParts,
|
||||
plate.WorkArea(),
|
||||
plate.PartSpacing,
|
||||
direction
|
||||
);
|
||||
}
|
||||
|
||||
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
public static double PushBoundingBox(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
@@ -206,7 +261,11 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
var reverseGap = SpatialQuery.DirectionalGap(
|
||||
movingBox,
|
||||
obstacleBoxes[i],
|
||||
opposite
|
||||
);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
@@ -219,7 +278,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing - 0.002;
|
||||
if (d < 0) d = 0;
|
||||
if (d < 0)
|
||||
d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
@@ -240,8 +300,13 @@ namespace OpenNest.Engine.Fill
|
||||
/// Repeatedly pushes parts left then down until total movement per
|
||||
/// iteration falls below the given threshold.
|
||||
/// </summary>
|
||||
public static void Settle(List<Part> parts, Box workArea, double partSpacing,
|
||||
double threshold = 0.01, int maxIterations = 20)
|
||||
public static void Settle(
|
||||
List<Part> parts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
double threshold = 0.01,
|
||||
int maxIterations = 20
|
||||
)
|
||||
{
|
||||
if (parts.Count < 2)
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -23,9 +23,12 @@ namespace OpenNest.Engine.Fill
|
||||
halfSpacing = partSpacing / 2;
|
||||
}
|
||||
|
||||
public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
|
||||
public List<Part> Fill(
|
||||
Drawing drawing,
|
||||
double rotationAngle = 0,
|
||||
CancellationToken token = default,
|
||||
Action<List<Part>, string> reportProgress = null)
|
||||
Action<List<Part>, string> reportProgress = null
|
||||
)
|
||||
{
|
||||
var pair = BuildPair(drawing, rotationAngle);
|
||||
if (pair == null)
|
||||
@@ -64,8 +67,10 @@ namespace OpenNest.Engine.Fill
|
||||
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
|
||||
|
||||
// Check that each part fits in the work area individually.
|
||||
if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return null;
|
||||
|
||||
// Slide part2 toward part1 from the right using geometry-aware distance.
|
||||
@@ -80,7 +85,11 @@ namespace OpenNest.Engine.Fill
|
||||
// Slide part2 left toward part1.
|
||||
var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left);
|
||||
var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right);
|
||||
var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
|
||||
var dist = SpatialQuery.DirectionalDistance(
|
||||
movingLines,
|
||||
stationaryLines,
|
||||
PushDirection.Left
|
||||
);
|
||||
|
||||
if (dist < double.MaxValue && dist > 0)
|
||||
{
|
||||
@@ -93,8 +102,10 @@ namespace OpenNest.Engine.Fill
|
||||
return null;
|
||||
|
||||
// Verify pair fits in work area.
|
||||
if (pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return null;
|
||||
|
||||
return pair;
|
||||
@@ -121,8 +132,14 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
// Find minimum distance from test pair sliding down toward original pair.
|
||||
var copyDistance = FindVerticalCopyDistance(
|
||||
pair.Part1, pair.Part2, testPart1, testPart2,
|
||||
boundary1, boundary2, pairHeight);
|
||||
pair.Part1,
|
||||
pair.Part2,
|
||||
testPart1,
|
||||
testPart2,
|
||||
boundary1,
|
||||
boundary2,
|
||||
pairHeight
|
||||
);
|
||||
|
||||
if (copyDistance <= 0)
|
||||
return column;
|
||||
@@ -144,25 +161,56 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private double FindVerticalCopyDistance(
|
||||
Part origPart1, Part origPart2,
|
||||
Part testPart1, Part testPart2,
|
||||
PartBoundary boundary1, PartBoundary boundary2,
|
||||
double pairHeight)
|
||||
Part origPart1,
|
||||
Part origPart2,
|
||||
Part testPart1,
|
||||
Part testPart2,
|
||||
PartBoundary boundary1,
|
||||
PartBoundary boundary2,
|
||||
double pairHeight
|
||||
)
|
||||
{
|
||||
// Check all 4 combinations: test parts sliding down toward original parts.
|
||||
var slidePairs = new[]
|
||||
{
|
||||
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
|
||||
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
|
||||
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
|
||||
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
|
||||
(
|
||||
moving: boundary1,
|
||||
movingLoc: testPart1.Location,
|
||||
stationary: boundary1,
|
||||
stationaryLoc: origPart1.Location
|
||||
),
|
||||
(
|
||||
moving: boundary1,
|
||||
movingLoc: testPart1.Location,
|
||||
stationary: boundary2,
|
||||
stationaryLoc: origPart2.Location
|
||||
),
|
||||
(
|
||||
moving: boundary2,
|
||||
movingLoc: testPart2.Location,
|
||||
stationary: boundary1,
|
||||
stationaryLoc: origPart1.Location
|
||||
),
|
||||
(
|
||||
moving: boundary2,
|
||||
movingLoc: testPart2.Location,
|
||||
stationary: boundary2,
|
||||
stationaryLoc: origPart2.Location
|
||||
),
|
||||
};
|
||||
|
||||
var minSlide = double.MaxValue;
|
||||
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
|
||||
{
|
||||
var d = SlideDistance(moving, movingLoc, stationary, stationaryLoc, PushDirection.Down);
|
||||
if (d < minSlide) minSlide = d;
|
||||
var d = SlideDistance(
|
||||
moving,
|
||||
movingLoc,
|
||||
stationary,
|
||||
stationaryLoc,
|
||||
PushDirection.Down
|
||||
);
|
||||
if (d < minSlide)
|
||||
minSlide = d;
|
||||
}
|
||||
|
||||
if (minSlide >= double.MaxValue || minSlide < 0)
|
||||
@@ -177,18 +225,24 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static double SlideDistance(
|
||||
PartBoundary movingBoundary, Vector movingLocation,
|
||||
PartBoundary stationaryBoundary, Vector stationaryLocation,
|
||||
PushDirection direction)
|
||||
PartBoundary movingBoundary,
|
||||
Vector movingLocation,
|
||||
PartBoundary stationaryBoundary,
|
||||
Vector stationaryLocation,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var movingEdges = movingBoundary.GetEdges(direction);
|
||||
var stationaryEdges = stationaryBoundary.GetEdges(opposite);
|
||||
|
||||
return SpatialQuery.DirectionalDistance(
|
||||
movingEdges, movingLocation,
|
||||
stationaryEdges, stationaryLocation,
|
||||
direction);
|
||||
movingEdges,
|
||||
movingLocation,
|
||||
stationaryEdges,
|
||||
stationaryLocation,
|
||||
direction
|
||||
);
|
||||
}
|
||||
|
||||
// --- Step 3: Iterative Adjustment ---
|
||||
@@ -249,7 +303,11 @@ namespace OpenNest.Engine.Fill
|
||||
return TryShiftDirection(pair, -adjustment, originalPairWidth);
|
||||
}
|
||||
|
||||
private PartPair? TryShiftDirection(PartPair pair, double verticalShift, double originalPairWidth)
|
||||
private PartPair? TryShiftDirection(
|
||||
PartPair pair,
|
||||
double verticalShift,
|
||||
double originalPairWidth
|
||||
)
|
||||
{
|
||||
// Clone parts so we don't mutate the originals.
|
||||
var p1 = (Part)pair.Part1.Clone();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -34,9 +34,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static PushDirection GetPushDirection(NestDirection direction)
|
||||
{
|
||||
return direction == NestDirection.Horizontal
|
||||
? PushDirection.Left
|
||||
: PushDirection.Down;
|
||||
return direction == NestDirection.Horizontal ? PushDirection.Left : PushDirection.Down;
|
||||
}
|
||||
|
||||
private static double GetDimension(Box box, NestDirection direction)
|
||||
@@ -75,10 +73,15 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
|
||||
var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
|
||||
partA.CloneAtOffset(offset), HalfSpacing);
|
||||
partA.CloneAtOffset(offset),
|
||||
HalfSpacing
|
||||
);
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEntities, stationaryEntities, pushDir);
|
||||
movingEntities,
|
||||
stationaryEntities,
|
||||
pushDir
|
||||
);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
return bboxDim + PartSpacing;
|
||||
@@ -140,12 +143,19 @@ namespace OpenNest.Engine.Fill
|
||||
continue;
|
||||
|
||||
stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[i], HalfSpacing);
|
||||
parts[i],
|
||||
HalfSpacing
|
||||
);
|
||||
movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[j].CloneAtOffset(offset), HalfSpacing);
|
||||
parts[j].CloneAtOffset(offset),
|
||||
HalfSpacing
|
||||
);
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEntities[j], stationaryEntities[i], pushDir);
|
||||
movingEntities[j],
|
||||
stationaryEntities[i],
|
||||
pushDir
|
||||
);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
continue;
|
||||
@@ -209,10 +219,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var part = basePart.CloneAtOffset(offset);
|
||||
|
||||
if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon &&
|
||||
part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon &&
|
||||
part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon &&
|
||||
part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon)
|
||||
if (
|
||||
part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon
|
||||
&& part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon
|
||||
&& part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon
|
||||
&& part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon
|
||||
)
|
||||
{
|
||||
result.Add(part);
|
||||
}
|
||||
@@ -258,7 +270,11 @@ namespace OpenNest.Engine.Fill
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool HasOverlappingParts(List<Part> parts, out int overlapA, out int overlapB)
|
||||
private static bool HasOverlappingParts(
|
||||
List<Part> parts,
|
||||
out int overlapA,
|
||||
out int overlapB
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
@@ -268,10 +284,10 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var b2 = parts[j].BoundingBox;
|
||||
|
||||
var overlapX = System.Math.Min(b1.Right, b2.Right)
|
||||
- System.Math.Max(b1.Left, b2.Left);
|
||||
var overlapY = System.Math.Min(b1.Top, b2.Top)
|
||||
- System.Math.Max(b1.Bottom, b2.Bottom);
|
||||
var overlapX =
|
||||
System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
|
||||
var overlapY =
|
||||
System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
|
||||
|
||||
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
|
||||
continue;
|
||||
@@ -305,8 +321,10 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
template.Offset(WorkArea.Location - template.BoundingBox.Location);
|
||||
|
||||
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||
template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|
||||
|| template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return pattern;
|
||||
|
||||
pattern.Parts.Add(template);
|
||||
@@ -367,8 +385,14 @@ namespace OpenNest.Engine.Fill
|
||||
return gridResult;
|
||||
}
|
||||
|
||||
private void LogOverlap(string step, NestDirection tilingDir,
|
||||
Pattern pattern, List<Part> parts, int idxA, int idxB)
|
||||
private void LogOverlap(
|
||||
string step,
|
||||
NestDirection tilingDir,
|
||||
Pattern pattern,
|
||||
List<Part> parts,
|
||||
int idxA,
|
||||
int idxB
|
||||
)
|
||||
{
|
||||
var pa = parts[idxA];
|
||||
var pb = parts[idxB];
|
||||
@@ -377,22 +401,32 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})");
|
||||
Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}");
|
||||
Debug.WriteLine($" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}");
|
||||
Debug.WriteLine($" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}");
|
||||
Debug.WriteLine(
|
||||
$" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}"
|
||||
);
|
||||
Debug.WriteLine($" Total parts after tiling: {parts.Count}");
|
||||
Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:");
|
||||
Debug.WriteLine($" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " +
|
||||
$"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})");
|
||||
Debug.WriteLine($" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " +
|
||||
$"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})");
|
||||
Debug.WriteLine(
|
||||
$" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° "
|
||||
+ $"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° "
|
||||
+ $"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})"
|
||||
);
|
||||
|
||||
// Log all pattern seed parts for reproduction
|
||||
Debug.WriteLine($" Pattern seed parts:");
|
||||
for (var i = 0; i < pattern.Parts.Count; i++)
|
||||
{
|
||||
var p = pattern.Parts[i];
|
||||
Debug.WriteLine($" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " +
|
||||
$"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}");
|
||||
Debug.WriteLine(
|
||||
$" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° "
|
||||
+ $"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,8 +480,10 @@ namespace OpenNest.Engine.Fill
|
||||
var offset = WorkArea.Location - pattern.BoundingBox.Location;
|
||||
var basePattern = pattern.Clone(offset);
|
||||
|
||||
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|
||||
|| basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return new List<Part>();
|
||||
|
||||
return FillGrid(basePattern, primaryAxis);
|
||||
|
||||
@@ -76,9 +76,10 @@ public static class FillResultCache
|
||||
}
|
||||
|
||||
public bool Equals(CacheKey other) =>
|
||||
ReferenceEquals(Drawing, other.Drawing) &&
|
||||
Width == other.Width && Height == other.Height &&
|
||||
Spacing == other.Spacing;
|
||||
ReferenceEquals(Drawing, other.Drawing)
|
||||
&& Width == other.Width
|
||||
&& Height == other.Height
|
||||
&& Spacing == other.Spacing;
|
||||
|
||||
public override bool Equals(object obj) => obj is CacheKey other && Equals(other);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -37,10 +37,14 @@ namespace OpenNest.Engine.Fill
|
||||
totalPartArea += part.BaseDrawing.Area;
|
||||
var bb = part.BoundingBox;
|
||||
|
||||
if (bb.Left < minX) minX = bb.Left;
|
||||
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||
if (bb.Right > maxX) maxX = bb.Right;
|
||||
if (bb.Top > maxY) maxY = bb.Top;
|
||||
if (bb.Left < minX)
|
||||
minX = bb.Left;
|
||||
if (bb.Bottom < minY)
|
||||
minY = bb.Bottom;
|
||||
if (bb.Right > maxX)
|
||||
maxX = bb.Right;
|
||||
if (bb.Top > maxY)
|
||||
maxY = bb.Top;
|
||||
}
|
||||
|
||||
var bboxArea = (maxX - minX) * (maxY - minY);
|
||||
@@ -63,8 +67,11 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
public static bool operator >(FillScore a, FillScore b) => a.CompareTo(b) > 0;
|
||||
|
||||
public static bool operator <(FillScore a, FillScore b) => a.CompareTo(b) < 0;
|
||||
|
||||
public static bool operator >=(FillScore a, FillScore b) => a.CompareTo(b) >= 0;
|
||||
|
||||
public static bool operator <=(FillScore a, FillScore b) => a.CompareTo(b) <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ public class GridDedup
|
||||
/// <summary>
|
||||
/// Gets or creates a GridDedup from FillContext.SharedState.
|
||||
/// </summary>
|
||||
public static GridDedup GetOrCreate(System.Collections.Generic.Dictionary<string, object> sharedState)
|
||||
public static GridDedup GetOrCreate(
|
||||
System.Collections.Generic.Dictionary<string, object> sharedState
|
||||
)
|
||||
{
|
||||
if (sharedState.TryGetValue(SharedStateKey, out var existing))
|
||||
return (GridDedup)existing;
|
||||
@@ -41,7 +43,11 @@ public class GridDedup
|
||||
|
||||
private readonly struct GridKey : IEquatable<GridKey>
|
||||
{
|
||||
private readonly int _patternW, _patternL, _workW, _workL, _dir;
|
||||
private readonly int _patternW,
|
||||
_patternL,
|
||||
_workW,
|
||||
_workL,
|
||||
_dir;
|
||||
|
||||
public GridKey(Box patternBox, Box workArea, NestDirection dir)
|
||||
{
|
||||
@@ -53,9 +59,11 @@ public class GridDedup
|
||||
}
|
||||
|
||||
public bool Equals(GridKey other) =>
|
||||
_patternW == other._patternW && _patternL == other._patternL &&
|
||||
_workW == other._workW && _workL == other._workL &&
|
||||
_dir == other._dir;
|
||||
_patternW == other._patternW
|
||||
&& _patternL == other._patternL
|
||||
&& _workW == other._workW
|
||||
&& _workL == other._workL
|
||||
&& _dir == other._dir;
|
||||
|
||||
public override bool Equals(object obj) => obj is GridKey other && Equals(other);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill
|
||||
return candExtent < currExtent;
|
||||
|
||||
return FillScore.Compute(candidate, workArea).Density
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
}
|
||||
|
||||
private static double YExtent(List<Part> parts)
|
||||
@@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var bb = part.BoundingBox;
|
||||
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||
if (bb.Top > maxY) maxY = bb.Top;
|
||||
if (bb.Bottom < minY)
|
||||
minY = bb.Bottom;
|
||||
if (bb.Top > maxY)
|
||||
maxY = bb.Top;
|
||||
}
|
||||
|
||||
return maxY - minY;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -33,7 +33,8 @@ namespace OpenNest.Engine.Fill
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null,
|
||||
int plateNumber = 0,
|
||||
Func<NestItem, Box, List<Part>> widthFillFunc = null)
|
||||
Func<NestItem, Box, List<Part>> widthFillFunc = null
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new IterativeShrinkResult();
|
||||
@@ -48,19 +49,20 @@ namespace OpenNest.Engine.Fill
|
||||
if (item.Quantity <= 0)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
var estimatedMax = bbox.Area() > 0
|
||||
? (int)(workArea.Area() / bbox.Area()) * 2
|
||||
: 1000;
|
||||
var estimatedMax =
|
||||
bbox.Area() > 0 ? (int)(workArea.Area() / bbox.Area()) * 2 : 1000;
|
||||
|
||||
workItems.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = System.Math.Max(1, estimatedMax),
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
workItems.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = System.Math.Max(1, estimatedMax),
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -86,10 +88,32 @@ namespace OpenNest.Engine.Fill
|
||||
ShrinkResult widthResult = null;
|
||||
|
||||
Parallel.Invoke(
|
||||
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
|
||||
() => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
|
||||
() =>
|
||||
heightResult = ShrinkFiller.Shrink(
|
||||
fillFunc,
|
||||
ni,
|
||||
box,
|
||||
spacing,
|
||||
ShrinkAxis.Length,
|
||||
token,
|
||||
targetCount: target,
|
||||
progress: progress,
|
||||
plateNumber: plateNumber,
|
||||
placedParts: placedSoFar
|
||||
),
|
||||
() =>
|
||||
widthResult = ShrinkFiller.Shrink(
|
||||
wFillFunc,
|
||||
ni,
|
||||
box,
|
||||
spacing,
|
||||
ShrinkAxis.Width,
|
||||
token,
|
||||
targetCount: target,
|
||||
progress: progress,
|
||||
plateNumber: plateNumber,
|
||||
placedParts: placedSoFar
|
||||
)
|
||||
);
|
||||
|
||||
var heightScore = FillScore.Compute(heightResult.Parts, box);
|
||||
@@ -112,15 +136,18 @@ namespace OpenNest.Engine.Fill
|
||||
var allParts = new List<Part>(placedSoFar.Count + best.Count);
|
||||
allParts.AddRange(placedSoFar);
|
||||
allParts.AddRange(best);
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = box,
|
||||
Description = $"Shrink: {best.Count} parts placed",
|
||||
IsOverallBest = true,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = box,
|
||||
Description = $"Shrink: {best.Count} parts placed",
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Accumulate for the next item's progress reports.
|
||||
@@ -136,8 +163,7 @@ namespace OpenNest.Engine.Fill
|
||||
var leftovers = new List<NestItem>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var placedCount = placed.Count(p =>
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
var placedCount = placed.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
|
||||
if (item.Quantity <= 0)
|
||||
continue; // unlimited items are always "satisfied" — no leftover
|
||||
@@ -145,15 +171,17 @@ namespace OpenNest.Engine.Fill
|
||||
var remaining = item.Quantity - placedCount;
|
||||
if (remaining > 0)
|
||||
{
|
||||
leftovers.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = remaining,
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
leftovers.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = remaining,
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,29 +193,43 @@ namespace OpenNest.Engine.Fill
|
||||
/// a staircase profile that maximizes usable remnant area.
|
||||
/// </summary>
|
||||
internal static void SortColumnsByHeight(List<Part> parts, double spacing) =>
|
||||
SortStrips(parts, spacing,
|
||||
primaryEdge: b => b.Left, extentEdge: b => b.Right,
|
||||
sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight,
|
||||
makeOffset: d => new Vector(d, 0));
|
||||
SortStrips(
|
||||
parts,
|
||||
spacing,
|
||||
primaryEdge: b => b.Left,
|
||||
extentEdge: b => b.Right,
|
||||
sortMetric: MaxTop,
|
||||
stripMin: MinLeft,
|
||||
stripMax: MaxRight,
|
||||
makeOffset: d => new Vector(d, 0)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts pair rows by width (narrowest first on the bottom) to create
|
||||
/// a staircase profile on the right side that maximizes usable remnant area.
|
||||
/// </summary>
|
||||
internal static void SortRowsByWidth(List<Part> parts, double spacing) =>
|
||||
SortStrips(parts, spacing,
|
||||
primaryEdge: b => b.Bottom, extentEdge: b => b.Top,
|
||||
sortMetric: MaxRight, stripMin: MinBottom, stripMax: MaxTop,
|
||||
makeOffset: d => new Vector(0, d));
|
||||
SortStrips(
|
||||
parts,
|
||||
spacing,
|
||||
primaryEdge: b => b.Bottom,
|
||||
extentEdge: b => b.Top,
|
||||
sortMetric: MaxRight,
|
||||
stripMin: MinBottom,
|
||||
stripMax: MaxTop,
|
||||
makeOffset: d => new Vector(0, d)
|
||||
);
|
||||
|
||||
private static void SortStrips(
|
||||
List<Part> parts, double spacing,
|
||||
List<Part> parts,
|
||||
double spacing,
|
||||
Func<Box, double> primaryEdge,
|
||||
Func<Box, double> extentEdge,
|
||||
Func<List<Part>, double> sortMetric,
|
||||
Func<List<Part>, double> stripMin,
|
||||
Func<List<Part>, double> stripMax,
|
||||
Func<double, Vector> makeOffset)
|
||||
Func<double, Vector> makeOffset
|
||||
)
|
||||
{
|
||||
if (parts == null || parts.Count <= 1)
|
||||
return;
|
||||
@@ -250,7 +292,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var max = double.MinValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Top > max) max = p.BoundingBox.Top;
|
||||
if (p.BoundingBox.Top > max)
|
||||
max = p.BoundingBox.Top;
|
||||
return max;
|
||||
}
|
||||
|
||||
@@ -258,7 +301,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var max = double.MinValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Right > max) max = p.BoundingBox.Right;
|
||||
if (p.BoundingBox.Right > max)
|
||||
max = p.BoundingBox.Right;
|
||||
return max;
|
||||
}
|
||||
|
||||
@@ -266,7 +310,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var min = double.MaxValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Left < min) min = p.BoundingBox.Left;
|
||||
if (p.BoundingBox.Left < min)
|
||||
min = p.BoundingBox.Left;
|
||||
return min;
|
||||
}
|
||||
|
||||
@@ -274,7 +319,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var min = double.MaxValue;
|
||||
foreach (var p in row)
|
||||
if (p.BoundingBox.Bottom < min) min = p.BoundingBox.Bottom;
|
||||
if (p.BoundingBox.Bottom < min)
|
||||
min = p.BoundingBox.Bottom;
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -9,6 +5,10 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -44,28 +44,49 @@ namespace OpenNest.Engine.Fill
|
||||
this.dedup = dedup ?? new GridDedup();
|
||||
}
|
||||
|
||||
public PairFillResult Fill(NestItem item, Box workArea,
|
||||
public PairFillResult Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
CancellationToken token = default,
|
||||
Action<List<Part>, string> reportProgress = null)
|
||||
Action<List<Part>, string> reportProgress = null
|
||||
)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, plateSize.Length, plateSize.Width, partSpacing);
|
||||
item.Drawing,
|
||||
plateSize.Length,
|
||||
plateSize.Width,
|
||||
partSpacing
|
||||
);
|
||||
|
||||
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
|
||||
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"
|
||||
);
|
||||
|
||||
var targetCount = item.Quantity > 0 ? item.Quantity : 0;
|
||||
var parts = EvaluateCandidates(candidates, item.Drawing, workArea, targetCount,
|
||||
token, reportProgress);
|
||||
var parts = EvaluateCandidates(
|
||||
candidates,
|
||||
item.Drawing,
|
||||
workArea,
|
||||
targetCount,
|
||||
token,
|
||||
reportProgress
|
||||
);
|
||||
|
||||
return new PairFillResult { Parts = parts, BestFits = bestFits };
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidates(
|
||||
List<BestFitResult> candidates, Drawing drawing,
|
||||
Box workArea, int targetCount,
|
||||
CancellationToken token, Action<List<Part>, string> reportProgress)
|
||||
List<BestFitResult> candidates,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
int targetCount,
|
||||
CancellationToken token,
|
||||
Action<List<Part>, string> reportProgress
|
||||
)
|
||||
{
|
||||
List<Part> best = null;
|
||||
var sinceImproved = 0;
|
||||
@@ -89,14 +110,23 @@ namespace OpenNest.Engine.Fill
|
||||
var minCountToBeat = best?.Count ?? 0;
|
||||
|
||||
var results = new List<Part>[batchCount];
|
||||
Parallel.For(0, batchCount,
|
||||
Parallel.For(
|
||||
0,
|
||||
batchCount,
|
||||
new ParallelOptions { CancellationToken = token },
|
||||
j =>
|
||||
{
|
||||
results[j] = EvaluateCandidate(
|
||||
candidates[batchStart + j], drawing, batchWorkArea,
|
||||
minCountToBeat, maxUtilization, partArea, token);
|
||||
});
|
||||
candidates[batchStart + j],
|
||||
drawing,
|
||||
batchWorkArea,
|
||||
minCountToBeat,
|
||||
maxUtilization,
|
||||
partArea,
|
||||
token
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
for (var j = 0; j < batchCount; j++)
|
||||
{
|
||||
@@ -104,20 +134,29 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
best = results[j];
|
||||
sinceImproved = 0;
|
||||
effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea);
|
||||
effectiveWorkArea = TryReduceWorkArea(
|
||||
best,
|
||||
targetCount,
|
||||
workArea,
|
||||
effectiveWorkArea
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
sinceImproved++;
|
||||
}
|
||||
|
||||
reportProgress?.Invoke(best,
|
||||
$"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
|
||||
reportProgress?.Invoke(
|
||||
best,
|
||||
$"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts"
|
||||
);
|
||||
}
|
||||
|
||||
if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +174,12 @@ namespace OpenNest.Engine.Fill
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
private static Box TryReduceWorkArea(List<Part> parts, int targetCount, Box workArea, Box effectiveWorkArea)
|
||||
private static Box TryReduceWorkArea(
|
||||
List<Part> parts,
|
||||
int targetCount,
|
||||
Box workArea,
|
||||
Box effectiveWorkArea
|
||||
)
|
||||
{
|
||||
if (targetCount <= 0 || parts.Count <= targetCount)
|
||||
return effectiveWorkArea;
|
||||
@@ -144,7 +188,9 @@ namespace OpenNest.Engine.Fill
|
||||
if (reduced.Area() >= effectiveWorkArea.Area())
|
||||
return effectiveWorkArea;
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)"
|
||||
);
|
||||
return reduced;
|
||||
}
|
||||
|
||||
@@ -158,23 +204,30 @@ namespace OpenNest.Engine.Fill
|
||||
if (parts.Count <= targetCount)
|
||||
return workArea;
|
||||
|
||||
var sorted = parts
|
||||
.OrderByDescending(p => p.BoundingBox.Top)
|
||||
.ToList();
|
||||
var sorted = parts.OrderByDescending(p => p.BoundingBox.Top).ToList();
|
||||
|
||||
var trimCount = sorted.Count - targetCount;
|
||||
var remaining = sorted.Skip(trimCount).ToList();
|
||||
|
||||
var newTop = remaining.Max(p => p.BoundingBox.Top);
|
||||
|
||||
return new Box(workArea.X, workArea.Y,
|
||||
return new Box(
|
||||
workArea.X,
|
||||
workArea.Y,
|
||||
workArea.Length,
|
||||
System.Math.Min(newTop - workArea.Y, workArea.Width));
|
||||
System.Math.Min(newTop - workArea.Y, workArea.Width)
|
||||
);
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
|
||||
Box workArea, int minCountToBeat, double maxUtilization, double partArea,
|
||||
CancellationToken token)
|
||||
private List<Part> EvaluateCandidate(
|
||||
BestFitResult candidate,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
int minCountToBeat,
|
||||
double maxUtilization,
|
||||
double partArea,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
var angles = BuildTilingAngles(candidate);
|
||||
@@ -211,10 +264,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var topCount = grids[0].Parts.Count;
|
||||
var optimisticRemnant = EstimateRemnantUpperBound(
|
||||
grids[0].Parts, workArea, maxUtilization, partArea);
|
||||
grids[0].Parts,
|
||||
workArea,
|
||||
maxUtilization,
|
||||
partArea
|
||||
);
|
||||
if (topCount + optimisticRemnant <= minCountToBeat)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +289,11 @@ namespace OpenNest.Engine.Fill
|
||||
if (best != null)
|
||||
{
|
||||
var remnantBound = EstimateRemnantUpperBound(
|
||||
gridParts, workArea, maxUtilization, partArea);
|
||||
gridParts,
|
||||
workArea,
|
||||
maxUtilization,
|
||||
partArea
|
||||
);
|
||||
if (gridParts.Count + remnantBound <= best.Count)
|
||||
break; // sorted descending, so remaining are even smaller
|
||||
}
|
||||
@@ -255,8 +318,12 @@ namespace OpenNest.Engine.Fill
|
||||
return best;
|
||||
}
|
||||
|
||||
private int EstimateRemnantUpperBound(List<Part> gridParts, Box workArea,
|
||||
double maxUtilization, double partArea)
|
||||
private int EstimateRemnantUpperBound(
|
||||
List<Part> gridParts,
|
||||
Box workArea,
|
||||
double maxUtilization,
|
||||
double partArea
|
||||
)
|
||||
{
|
||||
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||
|
||||
@@ -271,8 +338,12 @@ namespace OpenNest.Engine.Fill
|
||||
return (int)(remnantArea * maxUtilization / partArea) + 1;
|
||||
}
|
||||
|
||||
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
|
||||
Box workArea, CancellationToken token)
|
||||
private List<Part> FillRemnant(
|
||||
List<Part> gridParts,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||
var partBox = drawing.Program.BoundingBox();
|
||||
@@ -322,14 +393,19 @@ namespace OpenNest.Engine.Fill
|
||||
token.ThrowIfCancellationRequested();
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => filler.Fill(drawing, angle, dir),
|
||||
null, comparer, remnantBox);
|
||||
null,
|
||||
comparer,
|
||||
remnantBox
|
||||
);
|
||||
|
||||
if (result != null && result.Count > (parts?.Count ?? 0))
|
||||
parts = result;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
|
||||
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Remnant: {parts?.Count ?? 0} parts in "
|
||||
+ $"{remnantBox.Width:F2}x{remnantBox.Length:F2}"
|
||||
);
|
||||
|
||||
if (parts != null && parts.Count > 0)
|
||||
{
|
||||
@@ -365,16 +441,19 @@ namespace OpenNest.Engine.Fill
|
||||
if (workShortSide < plateShortSide * 0.5)
|
||||
{
|
||||
// Strip mode: prioritize candidates that fit the narrow dimension.
|
||||
var stripCandidates = kept
|
||||
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= MinStripUtilization)
|
||||
var stripCandidates = kept.Where(r =>
|
||||
r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= MinStripUtilization
|
||||
)
|
||||
.ToList();
|
||||
|
||||
SortByEstimatedCount(stripCandidates, workArea);
|
||||
|
||||
var top = stripCandidates.Take(MaxStripCandidates).ToList();
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"
|
||||
);
|
||||
return top;
|
||||
}
|
||||
|
||||
@@ -389,16 +468,18 @@ namespace OpenNest.Engine.Fill
|
||||
var w = workArea.Width;
|
||||
var l = workArea.Length;
|
||||
|
||||
candidates.Sort((a, b) =>
|
||||
{
|
||||
var aCount = EstimateTileCount(a, w, l);
|
||||
var bCount = EstimateTileCount(b, w, l);
|
||||
candidates.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var aCount = EstimateTileCount(a, w, l);
|
||||
var bCount = EstimateTileCount(b, w, l);
|
||||
|
||||
if (aCount != bCount)
|
||||
return bCount.CompareTo(aCount);
|
||||
if (aCount != bCount)
|
||||
return bCount.CompareTo(aCount);
|
||||
|
||||
return b.Utilization.CompareTo(a.Utilization);
|
||||
});
|
||||
return b.Utilization.CompareTo(a.Utilization);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private int EstimateTileCount(BestFitResult r, double areaW, double areaL)
|
||||
@@ -410,7 +491,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private int EstimateCount(double pairW, double pairH, double areaW, double areaL)
|
||||
{
|
||||
if (pairW <= 0 || pairH <= 0) return 0;
|
||||
if (pairW <= 0 || pairH <= 0)
|
||||
return 0;
|
||||
var cols = (int)((areaW + partSpacing) / (pairW + partSpacing));
|
||||
var rows = (int)((areaL + partSpacing) / (pairH + partSpacing));
|
||||
return cols * rows * 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -23,7 +23,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
public PartBoundary(Part part, double spacing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer == SpecialLayers.Cut)
|
||||
.ToList();
|
||||
|
||||
@@ -39,21 +40,29 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
// Circumscribe arcs so polygon vertices are always outside
|
||||
// the true arc — guarantees the boundary never under-estimates.
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true);
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(
|
||||
PolygonTolerance,
|
||||
circumscribe: true
|
||||
);
|
||||
polygon.RemoveSelfIntersections();
|
||||
_polygons.Add(polygon);
|
||||
}
|
||||
}
|
||||
|
||||
PrecomputeDirectionalEdges(
|
||||
out _leftEdges, out _rightEdges, out _upEdges, out _downEdges);
|
||||
out _leftEdges,
|
||||
out _rightEdges,
|
||||
out _upEdges,
|
||||
out _downEdges
|
||||
);
|
||||
}
|
||||
|
||||
private void PrecomputeDirectionalEdges(
|
||||
out (Vector start, Vector end)[] leftEdges,
|
||||
out (Vector start, Vector end)[] rightEdges,
|
||||
out (Vector start, Vector end)[] upEdges,
|
||||
out (Vector start, Vector end)[] downEdges)
|
||||
out (Vector start, Vector end)[] downEdges
|
||||
)
|
||||
{
|
||||
var left = new List<(Vector, Vector)>();
|
||||
var right = new List<(Vector, Vector)>();
|
||||
@@ -86,10 +95,14 @@ namespace OpenNest.Engine.Fill
|
||||
var dy = verts[i].Y - verts[i - 1].Y;
|
||||
var edge = (verts[i - 1], verts[i]);
|
||||
|
||||
if (-sign * dy > 0) left.Add(edge);
|
||||
if (sign * dy > 0) right.Add(edge);
|
||||
if (-sign * dx > 0) up.Add(edge);
|
||||
if (sign * dx > 0) down.Add(edge);
|
||||
if (-sign * dy > 0)
|
||||
left.Add(edge);
|
||||
if (sign * dy > 0)
|
||||
right.Add(edge);
|
||||
if (-sign * dx > 0)
|
||||
up.Add(edge);
|
||||
if (sign * dx > 0)
|
||||
down.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +158,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return _leftEdges;
|
||||
case PushDirection.Right: return _rightEdges;
|
||||
case PushDirection.Up: return _upEdges;
|
||||
case PushDirection.Down: return _downEdges;
|
||||
default: return _leftEdges;
|
||||
case PushDirection.Left:
|
||||
return _leftEdges;
|
||||
case PushDirection.Right:
|
||||
return _rightEdges;
|
||||
case PushDirection.Up:
|
||||
return _upEdges;
|
||||
case PushDirection.Down:
|
||||
return _downEdges;
|
||||
default:
|
||||
return _leftEdges;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -31,7 +31,8 @@ namespace OpenNest.Engine.Fill
|
||||
List<NestItem> items,
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null)
|
||||
IProgress<NestProgress> progress = null
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new List<Part>();
|
||||
@@ -60,13 +61,19 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static Dictionary<Drawing, int> BuildLocalQuantities(List<NestItem> items)
|
||||
{
|
||||
var localQty = new Dictionary<Drawing, int>(items.Count, ReferenceEqualityComparer.Instance);
|
||||
var localQty = new Dictionary<Drawing, int>(
|
||||
items.Count,
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
foreach (var item in items)
|
||||
localQty[item.Drawing] = item.Quantity;
|
||||
return localQty;
|
||||
}
|
||||
|
||||
private static double FindMinItemDimension(List<NestItem> items, Dictionary<Drawing, int> localQty)
|
||||
private static double FindMinItemDimension(
|
||||
List<NestItem> items,
|
||||
Dictionary<Drawing, int> localQty
|
||||
)
|
||||
{
|
||||
var minDim = double.MaxValue;
|
||||
foreach (var item in items)
|
||||
@@ -87,7 +94,8 @@ namespace OpenNest.Engine.Fill
|
||||
Dictionary<Drawing, int> localQty,
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
List<Part> allParts,
|
||||
CancellationToken token)
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
@@ -147,21 +155,30 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var p in parts)
|
||||
{
|
||||
var bb = p.BoundingBox;
|
||||
if (bb.Left < left) left = bb.Left;
|
||||
if (bb.Bottom < bottom) bottom = bb.Bottom;
|
||||
if (bb.Right > right) right = bb.Right;
|
||||
if (bb.Top > top) top = bb.Top;
|
||||
if (bb.Left < left)
|
||||
left = bb.Left;
|
||||
if (bb.Bottom < bottom)
|
||||
bottom = bb.Bottom;
|
||||
if (bb.Right > right)
|
||||
right = bb.Right;
|
||||
if (bb.Top > top)
|
||||
top = bb.Top;
|
||||
}
|
||||
|
||||
return new Box(left - spacing, bottom - spacing,
|
||||
right - left + spacing * 2, top - bottom + spacing * 2);
|
||||
return new Box(
|
||||
left - spacing,
|
||||
bottom - spacing,
|
||||
right - left + spacing * 2,
|
||||
top - bottom + spacing * 2
|
||||
);
|
||||
}
|
||||
|
||||
private static List<Part> TryFillInRemnants(
|
||||
NestItem item,
|
||||
int qty,
|
||||
List<Box> freeBoxes,
|
||||
Func<NestItem, Box, List<Part>> fillFunc)
|
||||
Func<NestItem, Box, List<Part>> fillFunc
|
||||
)
|
||||
{
|
||||
var itemBbox = item.Drawing.Program.BoundingBox();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -90,12 +90,14 @@ namespace OpenNest.Engine.Fill
|
||||
results.Add(new TieredRemnant(remnant, 1));
|
||||
}
|
||||
|
||||
results.Sort((a, b) =>
|
||||
{
|
||||
if (a.Priority != b.Priority)
|
||||
return a.Priority.CompareTo(b.Priority);
|
||||
return b.Box.Area().CompareTo(a.Box.Area());
|
||||
});
|
||||
results.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
if (a.Priority != b.Priority)
|
||||
return a.Priority.CompareTo(b.Priority);
|
||||
return b.Box.Area().CompareTo(a.Box.Area());
|
||||
}
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -125,11 +127,7 @@ namespace OpenNest.Engine.Fill
|
||||
ys.Add(obs.Top);
|
||||
}
|
||||
|
||||
var grid = new CellGrid
|
||||
{
|
||||
XCoords = xs.ToList(),
|
||||
YCoords = ys.ToList(),
|
||||
};
|
||||
var grid = new CellGrid { XCoords = xs.ToList(), YCoords = ys.ToList() };
|
||||
|
||||
grid.Cols = grid.XCoords.Count - 1;
|
||||
grid.Rows = grid.YCoords.Count - 1;
|
||||
@@ -146,9 +144,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
for (var c = 0; c < grid.Cols; c++)
|
||||
{
|
||||
var cell = new Box(grid.XCoords[c], grid.YCoords[r],
|
||||
var cell = new Box(
|
||||
grid.XCoords[c],
|
||||
grid.YCoords[r],
|
||||
grid.XCoords[c + 1] - grid.XCoords[c],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r]);
|
||||
grid.YCoords[r + 1] - grid.YCoords[r]
|
||||
);
|
||||
|
||||
grid.Empty[r, c] = !OverlapsAny(cell, clipped);
|
||||
}
|
||||
@@ -175,8 +176,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
foreach (var obs in obstacles)
|
||||
{
|
||||
if (cell.Left < obs.Right && cell.Right > obs.Left &&
|
||||
cell.Bottom < obs.Top && cell.Top > obs.Bottom)
|
||||
if (
|
||||
cell.Left < obs.Right
|
||||
&& cell.Right > obs.Left
|
||||
&& cell.Bottom < obs.Top
|
||||
&& cell.Top > obs.Bottom
|
||||
)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -227,24 +232,26 @@ namespace OpenNest.Engine.Fill
|
||||
private static bool IsContainedIn(Box inner, Box outer)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
return inner.Left >= outer.Left - eps &&
|
||||
inner.Right <= outer.Right + eps &&
|
||||
inner.Bottom >= outer.Bottom - eps &&
|
||||
inner.Top <= outer.Top + eps;
|
||||
return inner.Left >= outer.Left - eps
|
||||
&& inner.Right <= outer.Right + eps
|
||||
&& inner.Bottom >= outer.Bottom - eps
|
||||
&& inner.Top <= outer.Top + eps;
|
||||
}
|
||||
|
||||
private void SortByEdgeProximity(List<Box> boxes)
|
||||
{
|
||||
boxes.Sort((a, b) =>
|
||||
{
|
||||
var aEdge = TouchesEdge(a) ? 1 : 0;
|
||||
var bEdge = TouchesEdge(b) ? 1 : 0;
|
||||
boxes.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var aEdge = TouchesEdge(a) ? 1 : 0;
|
||||
var bEdge = TouchesEdge(b) ? 1 : 0;
|
||||
|
||||
if (aEdge != bEdge)
|
||||
return bEdge.CompareTo(aEdge);
|
||||
if (aEdge != bEdge)
|
||||
return bEdge.CompareTo(aEdge);
|
||||
|
||||
return b.Area().CompareTo(a.Area());
|
||||
});
|
||||
return b.Area().CompareTo(a.Area());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private bool TouchesEdge(Box box)
|
||||
@@ -264,30 +271,47 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
if (obs.Left < envLeft) envLeft = obs.Left;
|
||||
if (obs.Bottom < envBottom) envBottom = obs.Bottom;
|
||||
if (obs.Right > envRight) envRight = obs.Right;
|
||||
if (obs.Top > envTop) envTop = obs.Top;
|
||||
if (obs.Left < envLeft)
|
||||
envLeft = obs.Left;
|
||||
if (obs.Bottom < envBottom)
|
||||
envBottom = obs.Bottom;
|
||||
if (obs.Right > envRight)
|
||||
envRight = obs.Right;
|
||||
if (obs.Top > envTop)
|
||||
envTop = obs.Top;
|
||||
}
|
||||
|
||||
return new Box(envLeft, envBottom, envRight - envLeft, envTop - envBottom);
|
||||
}
|
||||
|
||||
private static void SplitAtEnvelope(Box remnant, Box envelope, double minDim, List<TieredRemnant> results)
|
||||
private static void SplitAtEnvelope(
|
||||
Box remnant,
|
||||
Box envelope,
|
||||
double minDim,
|
||||
List<TieredRemnant> results
|
||||
)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
|
||||
// Fully within the envelope.
|
||||
if (remnant.Left >= envelope.Left - eps && remnant.Right <= envelope.Right + eps &&
|
||||
remnant.Bottom >= envelope.Bottom - eps && remnant.Top <= envelope.Top + eps)
|
||||
if (
|
||||
remnant.Left >= envelope.Left - eps
|
||||
&& remnant.Right <= envelope.Right + eps
|
||||
&& remnant.Bottom >= envelope.Bottom - eps
|
||||
&& remnant.Top <= envelope.Top + eps
|
||||
)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fully outside the envelope (no overlap).
|
||||
if (remnant.Left >= envelope.Right - eps || remnant.Right <= envelope.Left + eps ||
|
||||
remnant.Bottom >= envelope.Top - eps || remnant.Top <= envelope.Bottom + eps)
|
||||
if (
|
||||
remnant.Left >= envelope.Right - eps
|
||||
|| remnant.Right <= envelope.Left + eps
|
||||
|| remnant.Bottom >= envelope.Top - eps
|
||||
|| remnant.Top <= envelope.Bottom + eps
|
||||
)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 2));
|
||||
return;
|
||||
@@ -300,36 +324,116 @@ namespace OpenNest.Engine.Fill
|
||||
var innerTop = System.Math.Min(remnant.Top, envelope.Top);
|
||||
|
||||
// Inner portion (priority 0).
|
||||
TryAdd(results, innerLeft, innerBottom, innerRight - innerLeft, innerTop - innerBottom, 0, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
innerBottom,
|
||||
innerRight - innerLeft,
|
||||
innerTop - innerBottom,
|
||||
0,
|
||||
minDim
|
||||
);
|
||||
|
||||
// Edge extensions (priority 1).
|
||||
if (remnant.Right > envelope.Right + eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, remnant.Width, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
remnant.Bottom,
|
||||
remnant.Right - envelope.Right,
|
||||
remnant.Width,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, remnant.Width, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
remnant.Bottom,
|
||||
envelope.Left - remnant.Left,
|
||||
remnant.Width,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, innerLeft, envelope.Top, innerRight - innerLeft, remnant.Top - envelope.Top, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
envelope.Top,
|
||||
innerRight - innerLeft,
|
||||
remnant.Top - envelope.Top,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, innerLeft, remnant.Bottom, innerRight - innerLeft, envelope.Bottom - remnant.Bottom, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
remnant.Bottom,
|
||||
innerRight - innerLeft,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
// Corner extensions (priority 2).
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, envelope.Right, envelope.Top, remnant.Right - envelope.Right, remnant.Top - envelope.Top, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
envelope.Top,
|
||||
remnant.Right - envelope.Right,
|
||||
remnant.Top - envelope.Top,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
remnant.Bottom,
|
||||
remnant.Right - envelope.Right,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, remnant.Left, envelope.Top, envelope.Left - remnant.Left, remnant.Top - envelope.Top, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
envelope.Top,
|
||||
envelope.Left - remnant.Left,
|
||||
remnant.Top - envelope.Top,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
remnant.Bottom,
|
||||
envelope.Left - remnant.Left,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
}
|
||||
|
||||
private static void TryAdd(List<TieredRemnant> results, double x, double y, double w, double h, int priority, double minDim)
|
||||
private static void TryAdd(
|
||||
List<TieredRemnant> results,
|
||||
double x,
|
||||
double y,
|
||||
double w,
|
||||
double h,
|
||||
int priority,
|
||||
double minDim
|
||||
)
|
||||
{
|
||||
if (w >= minDim && h >= minDim)
|
||||
results.Add(new TieredRemnant(new Box(x, y, w, h), priority));
|
||||
@@ -379,10 +483,14 @@ namespace OpenNest.Engine.Fill
|
||||
var top = stack.Pop();
|
||||
startCol = top.startCol;
|
||||
|
||||
candidates.Add(new Box(
|
||||
grid.XCoords[top.startCol], grid.YCoords[r - top.h + 1],
|
||||
grid.XCoords[c] - grid.XCoords[top.startCol],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1]));
|
||||
candidates.Add(
|
||||
new Box(
|
||||
grid.XCoords[top.startCol],
|
||||
grid.YCoords[r - top.h + 1],
|
||||
grid.XCoords[c] - grid.XCoords[top.startCol],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (h > 0)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -14,7 +14,8 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public static double FindBestRotation(NestItem item)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(item.Drawing.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(item.Drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
@@ -62,7 +63,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public enum ShrinkAxis { Width, Length }
|
||||
public enum ShrinkAxis
|
||||
{
|
||||
Width,
|
||||
Length,
|
||||
}
|
||||
|
||||
public class ShrinkResult
|
||||
{
|
||||
@@ -23,14 +27,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public static ShrinkResult Shrink(
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
NestItem item, Box box,
|
||||
NestItem item,
|
||||
Box box,
|
||||
double spacing,
|
||||
ShrinkAxis axis,
|
||||
CancellationToken token = default,
|
||||
int targetCount = 0,
|
||||
IProgress<NestProgress> progress = null,
|
||||
int plateNumber = 0,
|
||||
List<Part> placedParts = null)
|
||||
List<Part> placedParts = null
|
||||
)
|
||||
{
|
||||
var startBox = box;
|
||||
if (targetCount > 0)
|
||||
@@ -38,8 +44,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var parts = fillFunc(item, startBox);
|
||||
|
||||
if (targetCount > 0 && startBox != box
|
||||
&& (parts == null || parts.Count < targetCount))
|
||||
if (targetCount > 0 && startBox != box && (parts == null || parts.Count < targetCount))
|
||||
{
|
||||
parts = fillFunc(item, box);
|
||||
}
|
||||
@@ -47,9 +52,8 @@ namespace OpenNest.Engine.Fill
|
||||
if (parts == null || parts.Count == 0)
|
||||
return new ShrinkResult { Parts = parts ?? new List<Part>(), Dimension = 0 };
|
||||
|
||||
var shrinkTarget = targetCount > 0
|
||||
? System.Math.Min(targetCount, parts.Count)
|
||||
: parts.Count;
|
||||
var shrinkTarget =
|
||||
targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count;
|
||||
|
||||
if (parts.Count > shrinkTarget)
|
||||
parts = TrimToCount(parts, shrinkTarget, axis);
|
||||
@@ -62,16 +66,22 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static void ReportShrinkProgress(
|
||||
IProgress<NestProgress> progress, int plateNumber,
|
||||
List<Part> placedParts, List<Part> bestParts,
|
||||
Box workArea, ShrinkAxis axis, double dim)
|
||||
IProgress<NestProgress> progress,
|
||||
int plateNumber,
|
||||
List<Part> placedParts,
|
||||
List<Part> bestParts,
|
||||
Box workArea,
|
||||
ShrinkAxis axis,
|
||||
double dim
|
||||
)
|
||||
{
|
||||
if (progress == null)
|
||||
return;
|
||||
|
||||
var allParts = placedParts != null && placedParts.Count > 0
|
||||
? new List<Part>(placedParts.Count + bestParts.Count)
|
||||
: new List<Part>(bestParts.Count);
|
||||
var allParts =
|
||||
placedParts != null && placedParts.Count > 0
|
||||
? new List<Part>(placedParts.Count + bestParts.Count)
|
||||
: new List<Part>(bestParts.Count);
|
||||
|
||||
if (placedParts != null && placedParts.Count > 0)
|
||||
allParts.AddRange(placedParts);
|
||||
@@ -79,14 +89,17 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
|
||||
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = workArea,
|
||||
Description = desc,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = workArea,
|
||||
Description = desc,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,8 +107,14 @@ namespace OpenNest.Engine.Fill
|
||||
/// that fits roughly the target count. Scales the shrink axis proportionally
|
||||
/// from the full-area count down to the target, with margin.
|
||||
/// </summary>
|
||||
internal static Box EstimateStartBox(NestItem item, Box box,
|
||||
double spacing, ShrinkAxis axis, int targetCount, double marginFactor = 1.3)
|
||||
internal static Box EstimateStartBox(
|
||||
NestItem item,
|
||||
Box box,
|
||||
double spacing,
|
||||
ShrinkAxis axis,
|
||||
int targetCount,
|
||||
double marginFactor = 1.3
|
||||
)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
if (bbox.Width <= 0 || bbox.Length <= 0)
|
||||
@@ -105,7 +124,10 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
// Use FillBestFit for a fast, accurate rectangle count on the full box.
|
||||
var bin = new Bin { Size = new Size(box.Width, box.Length) };
|
||||
var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) };
|
||||
var packItem = new Item
|
||||
{
|
||||
Size = new Size(bbox.Width + spacing, bbox.Length + spacing),
|
||||
};
|
||||
var packer = new FillBestFit(bin);
|
||||
packer.Fill(packItem);
|
||||
var fullCount = bin.Items.Count;
|
||||
@@ -130,9 +152,7 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
||||
|
||||
return axis == ShrinkAxis.Width
|
||||
? placedBox.Right - box.X
|
||||
: placedBox.Top - box.Y;
|
||||
return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
@@ -7,7 +8,6 @@ using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace OpenNest.Engine.Fill;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class StripeFiller
|
||||
/// Factory to create the engine used for filling the remnant strip.
|
||||
/// Defaults to NestEngineRegistry.Create (uses the user's selected engine).
|
||||
/// </summary>
|
||||
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; }
|
||||
= NestEngineRegistry.Create;
|
||||
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; } =
|
||||
NestEngineRegistry.Create;
|
||||
|
||||
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
||||
{
|
||||
@@ -64,15 +64,27 @@ public class StripeFiller
|
||||
|
||||
foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||
{
|
||||
var perpAxis = axis == NestDirection.Horizontal
|
||||
? NestDirection.Vertical : NestDirection.Horizontal;
|
||||
var perpAxis =
|
||||
axis == NestDirection.Horizontal
|
||||
? NestDirection.Vertical
|
||||
: NestDirection.Horizontal;
|
||||
var sheetSpan = GetDimension(workArea, axis);
|
||||
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
||||
|
||||
var expandResult = ConvergeStripeAngle(
|
||||
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
);
|
||||
var shrinkResult = ConvergeStripeAngleShrink(
|
||||
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
);
|
||||
|
||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||
{
|
||||
@@ -84,9 +96,11 @@ public class StripeFiller
|
||||
if (result == null || result.Count == 0)
|
||||
continue;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " +
|
||||
$"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " +
|
||||
$"grid={result.Count} parts");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] {strategyName} candidate {i} {dirLabel}: "
|
||||
+ $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, "
|
||||
+ $"grid={result.Count} parts"
|
||||
);
|
||||
|
||||
if (_comparer.IsBetter(result, bestParts, workArea))
|
||||
{
|
||||
@@ -95,15 +109,21 @@ public class StripeFiller
|
||||
}
|
||||
}
|
||||
|
||||
_context.ReportProgress(bestParts,
|
||||
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts");
|
||||
_context.ReportProgress(
|
||||
bestParts,
|
||||
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"
|
||||
);
|
||||
}
|
||||
|
||||
return bestParts ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
||||
NestDirection primaryAxis, NestDirection perpAxis)
|
||||
private List<Part> BuildGrid(
|
||||
List<Part> pairParts,
|
||||
double angle,
|
||||
NestDirection primaryAxis,
|
||||
NestDirection perpAxis
|
||||
)
|
||||
{
|
||||
var workArea = _context.WorkArea;
|
||||
var spacing = _context.Plate.PartSpacing;
|
||||
@@ -123,8 +143,10 @@ public class StripeFiller
|
||||
|
||||
var partsPerStripe = stripeParts.Count;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Stripe: {partsPerStripe} parts, " +
|
||||
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] Stripe: {partsPerStripe} parts, "
|
||||
+ $"box={stripeBox.Width:F2}x{stripeBox.Length:F2}"
|
||||
);
|
||||
|
||||
var stripePattern = new Pattern();
|
||||
stripePattern.Parts.AddRange(stripeParts);
|
||||
@@ -141,8 +163,10 @@ public class StripeFiller
|
||||
var completeCount = gridParts.Count / partsPerStripe * partsPerStripe;
|
||||
if (completeCount < gridParts.Count)
|
||||
{
|
||||
Debug.WriteLine($"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " +
|
||||
$"(dropped {gridParts.Count - completeCount} partial)");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} "
|
||||
+ $"(dropped {gridParts.Count - completeCount} partial)"
|
||||
);
|
||||
gridParts = gridParts.GetRange(0, completeCount);
|
||||
}
|
||||
}
|
||||
@@ -184,12 +208,10 @@ public class StripeFiller
|
||||
_context.Item.Drawing,
|
||||
_context.Plate.Size.Length,
|
||||
_context.Plate.Size.Width,
|
||||
_context.Plate.PartSpacing);
|
||||
_context.Plate.PartSpacing
|
||||
);
|
||||
|
||||
return bestFits
|
||||
.Where(r => r.Keep)
|
||||
.Take(MaxPairCandidates)
|
||||
.ToList();
|
||||
return bestFits.Where(r => r.Keep).Take(MaxPairCandidates).ToList();
|
||||
}
|
||||
|
||||
private static Box MakeStripeBox(Box workArea, double perpDim, NestDirection primaryAxis)
|
||||
@@ -208,7 +230,8 @@ public class StripeFiller
|
||||
var gridBox = gridParts.GetBoundingBox();
|
||||
var minDim = System.Math.Min(
|
||||
drawing.Program.BoundingBox().Width,
|
||||
drawing.Program.BoundingBox().Length);
|
||||
drawing.Program.BoundingBox().Length
|
||||
);
|
||||
|
||||
Box remnantBox;
|
||||
|
||||
@@ -229,7 +252,9 @@ public class StripeFiller
|
||||
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Width);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}"
|
||||
);
|
||||
|
||||
var cachedResult = FillResultCache.Get(drawing, remnantBox, spacing);
|
||||
if (cachedResult != null)
|
||||
@@ -246,7 +271,10 @@ public class StripeFiller
|
||||
_context.Token.ThrowIfCancellationRequested();
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => filler.Fill(drawing, angle, dir),
|
||||
null, _comparer, remnantBox);
|
||||
null,
|
||||
_comparer,
|
||||
remnantBox
|
||||
);
|
||||
|
||||
if (result != null && result.Count > (best?.Count ?? 0))
|
||||
best = result;
|
||||
@@ -264,7 +292,10 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
public static double FindAngleForTargetSpan(
|
||||
List<Part> patternParts, double targetSpan, NestDirection axis)
|
||||
List<Part> patternParts,
|
||||
double targetSpan,
|
||||
NestDirection axis
|
||||
)
|
||||
{
|
||||
var bestAngle = 0.0;
|
||||
var bestDiff = double.MaxValue;
|
||||
@@ -292,8 +323,7 @@ public class StripeFiller
|
||||
var (a1, s1) = samples[i];
|
||||
var (a2, s2) = samples[i + 1];
|
||||
|
||||
if ((s1 <= targetSpan && targetSpan <= s2) ||
|
||||
(s2 <= targetSpan && targetSpan <= s1))
|
||||
if ((s1 <= targetSpan && targetSpan <= s2) || (s2 <= targetSpan && targetSpan <= s1))
|
||||
{
|
||||
var result = BisectForTarget(patternParts, a1, a2, targetSpan, axis);
|
||||
var resultSpan = GetRotatedSpan(patternParts, result, axis);
|
||||
@@ -332,8 +362,12 @@ public class StripeFiller
|
||||
/// Returns (angle, waste, pairCount).
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
||||
List<Part> patternParts, double sheetSpan, double spacing,
|
||||
NestDirection axis, CancellationToken token = default)
|
||||
List<Part> patternParts,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var startAngle = OrientShortSideAlong(patternParts, axis);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
||||
@@ -344,8 +378,12 @@ public class StripeFiller
|
||||
/// Complements ConvergeStripeAngle which only expands.
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink(
|
||||
List<Part> patternParts, double sheetSpan, double spacing,
|
||||
NestDirection axis, CancellationToken token = default)
|
||||
List<Part> patternParts,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
||||
var naturalPattern = FillHelpers.BuildRotatedPattern(patternParts, baseAngle);
|
||||
@@ -366,8 +404,13 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
||||
List<Part> patternParts, double startAngle, double sheetSpan,
|
||||
double spacing, NestDirection axis, CancellationToken token)
|
||||
List<Part> patternParts,
|
||||
double startAngle,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var bestWaste = double.MaxValue;
|
||||
var bestAngle = startAngle;
|
||||
@@ -381,15 +424,18 @@ public class StripeFiller
|
||||
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
||||
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
||||
var perpDim = axis == NestDirection.Horizontal
|
||||
? rotated.BoundingBox.Width : rotated.BoundingBox.Length;
|
||||
var perpDim =
|
||||
axis == NestDirection.Horizontal
|
||||
? rotated.BoundingBox.Width
|
||||
: rotated.BoundingBox.Length;
|
||||
|
||||
if (pairSpan + spacing <= 0)
|
||||
break;
|
||||
|
||||
var stripeBox = axis == NestDirection.Horizontal
|
||||
? new Box(0, 0, sheetSpan, perpDim)
|
||||
: new Box(0, 0, perpDim, sheetSpan);
|
||||
var stripeBox =
|
||||
axis == NestDirection.Horizontal
|
||||
? new Box(0, 0, sheetSpan, perpDim)
|
||||
: new Box(0, 0, perpDim, sheetSpan);
|
||||
var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" };
|
||||
var filled = engine.Fill(rotated, axis);
|
||||
var n = filled?.Count ?? 0;
|
||||
@@ -400,8 +446,10 @@ public class StripeFiller
|
||||
var filledBox = ((IEnumerable<IBoundable>)filled).GetBoundingBox();
|
||||
var remaining = sheetSpan - GetDimension(filledBox, axis);
|
||||
|
||||
Debug.WriteLine($"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " +
|
||||
$"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}");
|
||||
Debug.WriteLine(
|
||||
$"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, "
|
||||
+ $"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}"
|
||||
);
|
||||
|
||||
if (remaining < bestWaste)
|
||||
{
|
||||
@@ -414,7 +462,8 @@ public class StripeFiller
|
||||
break;
|
||||
|
||||
var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
||||
if (bboxN <= 0) bboxN = 1;
|
||||
if (bboxN <= 0)
|
||||
bboxN = 1;
|
||||
var delta = remaining / bboxN;
|
||||
var targetSpan = pairSpan + delta;
|
||||
|
||||
@@ -429,8 +478,12 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
private static double BisectForTarget(
|
||||
List<Part> patternParts, double lo, double hi,
|
||||
double targetSpan, NestDirection axis)
|
||||
List<Part> patternParts,
|
||||
double lo,
|
||||
double hi,
|
||||
double targetSpan,
|
||||
NestDirection axis
|
||||
)
|
||||
{
|
||||
var bestAngle = lo;
|
||||
var bestDiff = double.MaxValue;
|
||||
@@ -451,8 +504,10 @@ public class StripeFiller
|
||||
break;
|
||||
|
||||
var loSpan = GetRotatedSpan(patternParts, lo, axis);
|
||||
if ((loSpan < targetSpan && span < targetSpan) ||
|
||||
(loSpan > targetSpan && span > targetSpan))
|
||||
if (
|
||||
(loSpan < targetSpan && span < targetSpan)
|
||||
|| (loSpan > targetSpan && span > targetSpan)
|
||||
)
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
@@ -461,8 +516,7 @@ public class StripeFiller
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
private static double GetRotatedSpan(
|
||||
List<Part> patternParts, double angle, NestDirection axis)
|
||||
private static double GetRotatedSpan(List<Part> patternParts, double angle, NestDirection axis)
|
||||
{
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle);
|
||||
return axis == NestDirection.Horizontal
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill
|
||||
return candExtent < currExtent;
|
||||
|
||||
return FillScore.Compute(candidate, workArea).Density
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
}
|
||||
|
||||
private static double XExtent(List<Part> parts)
|
||||
@@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var bb = part.BoundingBox;
|
||||
if (bb.Left < minX) minX = bb.Left;
|
||||
if (bb.Right > maxX) maxX = bb.Right;
|
||||
if (bb.Left < minX)
|
||||
minX = bb.Left;
|
||||
if (bb.Right > maxX)
|
||||
maxX = bb.Right;
|
||||
}
|
||||
|
||||
return maxX - minX;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace OpenNest
|
||||
/// </summary>
|
||||
public class HorizontalRemnantEngine : DefaultNestEngine
|
||||
{
|
||||
public HorizontalRemnantEngine(Plate plate) : base(plate) { }
|
||||
public HorizontalRemnantEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Horizontal Remnant";
|
||||
|
||||
@@ -26,9 +27,17 @@ namespace OpenNest
|
||||
|
||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
)
|
||||
{
|
||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
var baseAngles = new List<double>
|
||||
{
|
||||
classification.PrimaryAngle,
|
||||
classification.PrimaryAngle + Angle.HalfPI,
|
||||
};
|
||||
baseAngles.Sort((a, b) => RotatedHeight(item, a).CompareTo(RotatedHeight(item, b)));
|
||||
return baseAngles;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
@@ -26,8 +26,12 @@ namespace OpenNest.Engine
|
||||
AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints);
|
||||
}
|
||||
|
||||
private Vector[] AssignPass(List<SequencedPart> sequenced, CuttingParameters parameters,
|
||||
Vector exitPoint, Vector[] nextPiercePoints)
|
||||
private Vector[] AssignPass(
|
||||
List<SequencedPart> sequenced,
|
||||
CuttingParameters parameters,
|
||||
Vector exitPoint,
|
||||
Vector[] nextPiercePoints
|
||||
)
|
||||
{
|
||||
var piercePoints = new Vector[sequenced.Count];
|
||||
var currentPoint = exitPoint;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.ML
|
||||
{
|
||||
@@ -16,8 +16,11 @@ namespace OpenNest.Engine.ML
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static List<double> PredictAngles(
|
||||
PartFeatures features, double sheetWidth, double sheetHeight,
|
||||
double threshold = 0.3)
|
||||
PartFeatures features,
|
||||
double sheetWidth,
|
||||
double sheetHeight,
|
||||
double threshold = 0.3
|
||||
)
|
||||
{
|
||||
var session = GetSession();
|
||||
if (session == null)
|
||||
@@ -41,7 +44,7 @@ namespace OpenNest.Engine.ML
|
||||
var tensor = new DenseTensor<float>(input, new[] { 1, 11 });
|
||||
var inputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("features", tensor)
|
||||
NamedOnnxValue.CreateFromTensor("features", tensor),
|
||||
};
|
||||
|
||||
using var results = session.Run(inputs);
|
||||
|
||||
@@ -24,24 +24,32 @@ namespace OpenNest.Engine.ML
|
||||
|
||||
public static class BruteForceRunner
|
||||
{
|
||||
public static BruteForceResult Run(Drawing drawing, Plate plate, bool forceFullAngleSweep = false)
|
||||
public static BruteForceResult Run(
|
||||
Drawing drawing,
|
||||
Plate plate,
|
||||
bool forceFullAngleSweep = false
|
||||
)
|
||||
{
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
engine.ForceFullAngleSweep = forceFullAngleSweep;
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
var parts = engine.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
sw.Stop();
|
||||
|
||||
if (parts == null || parts.Count == 0)
|
||||
return null;
|
||||
|
||||
// Rank phase results — winner is explicit, runners-up sorted by count.
|
||||
var winner = engine.PhaseResults
|
||||
.FirstOrDefault(r => r.Phase == engine.WinnerPhase);
|
||||
var runnerUps = engine.PhaseResults
|
||||
.Where(r => r.PartCount > 0 && r.Phase != engine.WinnerPhase)
|
||||
var winner = engine.PhaseResults.FirstOrDefault(r => r.Phase == engine.WinnerPhase);
|
||||
var runnerUps = engine
|
||||
.PhaseResults.Where(r => r.PartCount > 0 && r.Phase != engine.WinnerPhase)
|
||||
.OrderByDescending(r => r.PartCount)
|
||||
.ToList();
|
||||
|
||||
@@ -60,19 +68,27 @@ namespace OpenNest.Engine.ML
|
||||
ThirdPlaceEngine = runnerUps.Count > 1 ? runnerUps[1].Phase.ToString() : "",
|
||||
ThirdPlacePartCount = runnerUps.Count > 1 ? runnerUps[1].PartCount : 0,
|
||||
ThirdPlaceTimeMs = runnerUps.Count > 1 ? runnerUps[1].TimeMs : 0,
|
||||
AngleResults = engine.AngleResults.ToList()
|
||||
AngleResults = engine.AngleResults.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
private static string SerializeLayout(List<Part> parts)
|
||||
{
|
||||
var data = parts.Select(p => new { X = p.Location.X, Y = p.Location.Y, R = p.Rotation }).ToList();
|
||||
var data = parts
|
||||
.Select(p => new
|
||||
{
|
||||
X = p.Location.X,
|
||||
Y = p.Location.Y,
|
||||
R = p.Rotation,
|
||||
})
|
||||
.ToList();
|
||||
return System.Text.Json.JsonSerializer.Serialize(data);
|
||||
}
|
||||
|
||||
private static double CalculateUtilization(List<Part> parts, double plateArea)
|
||||
{
|
||||
if (plateArea <= 0) return 0;
|
||||
if (plateArea <= 0)
|
||||
return 0;
|
||||
return parts.Sum(p => p.BaseDrawing.Area) / plateArea;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.ML
|
||||
{
|
||||
@@ -7,10 +7,10 @@ namespace OpenNest.Engine.ML
|
||||
{
|
||||
// --- Geometric Features ---
|
||||
public double Area { get; set; }
|
||||
public double Convexity { get; set; } // Area / Convex Hull Area
|
||||
public double AspectRatio { get; set; } // Width / Length
|
||||
public double BoundingBoxFill { get; set; } // Area / (Width * Length)
|
||||
public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2
|
||||
public double Convexity { get; set; } // Area / Convex Hull Area
|
||||
public double AspectRatio { get; set; } // Width / Length
|
||||
public double BoundingBoxFill { get; set; } // Area / (Width * Length)
|
||||
public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2
|
||||
public double PerimeterToAreaRatio { get; set; } // Perimeter / Area — spacing sensitivity
|
||||
public int VertexCount { get; set; }
|
||||
|
||||
@@ -30,14 +30,16 @@ namespace OpenNest.Engine.ML
|
||||
// Normalize to canonical frame so features are invariant to import orientation.
|
||||
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||
|
||||
var entities = OpenNest.Converters.ConvertProgram.ToGeometry(canonical.Program)
|
||||
var entities = OpenNest
|
||||
.Converters.ConvertProgram.ToGeometry(canonical.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
var profile = new ShapeProfile(entities);
|
||||
var perimeter = profile.Perimeter;
|
||||
|
||||
if (perimeter == null) return null;
|
||||
if (perimeter == null)
|
||||
return null;
|
||||
|
||||
var polygon = perimeter.ToPolygonWithTolerance(0.01);
|
||||
polygon.UpdateBounds();
|
||||
@@ -53,12 +55,13 @@ namespace OpenNest.Engine.ML
|
||||
AspectRatio = bb.Length / (bb.Width > 0 ? bb.Width : 1.0),
|
||||
BoundingBoxFill = canonical.Area / (bb.Area() > 0 ? bb.Area() : 1.0),
|
||||
VertexCount = polygon.Vertices.Count,
|
||||
Bitmask = GenerateBitmask(polygon, 32)
|
||||
Bitmask = GenerateBitmask(polygon, 32),
|
||||
};
|
||||
|
||||
// Circularity = 4 * PI * Area / Perimeter^2
|
||||
var perimeterLen = polygon.Perimeter();
|
||||
features.Circularity = (4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen);
|
||||
features.Circularity =
|
||||
(4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen);
|
||||
features.PerimeterToAreaRatio = canonical.Area > 0 ? perimeterLen / canonical.Area : 0;
|
||||
|
||||
return features;
|
||||
|
||||
@@ -32,7 +32,9 @@ namespace OpenNest
|
||||
private MultiPlateNester(
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
_options = options;
|
||||
_template = options.Template;
|
||||
@@ -49,16 +51,20 @@ namespace OpenNest
|
||||
|
||||
public static bool FitsBounds(Box container, Box part)
|
||||
{
|
||||
var fitsNormal = container.Width >= part.Width - Tolerance.Epsilon
|
||||
&& container.Length >= part.Length - Tolerance.Epsilon;
|
||||
var fitsRotated = container.Width >= part.Length - Tolerance.Epsilon
|
||||
&& container.Length >= part.Width - Tolerance.Epsilon;
|
||||
var fitsNormal =
|
||||
container.Width >= part.Width - Tolerance.Epsilon
|
||||
&& container.Length >= part.Length - Tolerance.Epsilon;
|
||||
var fitsRotated =
|
||||
container.Width >= part.Length - Tolerance.Epsilon
|
||||
&& container.Length >= part.Width - Tolerance.Epsilon;
|
||||
return fitsNormal || fitsRotated;
|
||||
}
|
||||
|
||||
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
||||
{
|
||||
var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList();
|
||||
var withBounds = items
|
||||
.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox()))
|
||||
.ToList();
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
@@ -151,7 +157,8 @@ namespace OpenNest
|
||||
PlateOption upgradeSize,
|
||||
PlateOption newPlateSize,
|
||||
double salvageRate,
|
||||
double estimatedNewPlateUtilization)
|
||||
double estimatedNewPlateUtilization
|
||||
)
|
||||
{
|
||||
var upgradeCost = upgradeSize.Cost - currentSize.Cost;
|
||||
|
||||
@@ -175,7 +182,8 @@ namespace OpenNest
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates = null,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var nester = new MultiPlateNester(options, existingPlates, progress, token);
|
||||
return nester.Run(items, options.SortOrder, options.AllowPlateCreation);
|
||||
@@ -205,7 +213,8 @@ namespace OpenNest
|
||||
|
||||
var zoneAspect = zone.Width / zone.Length;
|
||||
var partAspect = partBounds.Width / partBounds.Length;
|
||||
var aspectMatch = System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect);
|
||||
var aspectMatch =
|
||||
System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect);
|
||||
|
||||
return utilization * 0.7 + aspectMatch * 0.3;
|
||||
}
|
||||
@@ -237,7 +246,8 @@ namespace OpenNest
|
||||
if (HasPlateOptions)
|
||||
{
|
||||
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length)
|
||||
);
|
||||
}
|
||||
|
||||
return pr;
|
||||
@@ -269,7 +279,11 @@ namespace OpenNest
|
||||
return pool;
|
||||
}
|
||||
|
||||
private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func<List<Box>, bool> tryFill)
|
||||
private bool TryWithUpgradedSize(
|
||||
PlateResult pr,
|
||||
PlateOption upgradeOption,
|
||||
Func<List<Box>, bool> tryFill
|
||||
)
|
||||
{
|
||||
var oldSize = pr.Plate.Size;
|
||||
var oldChosenSize = pr.ChosenSize;
|
||||
@@ -289,12 +303,18 @@ namespace OpenNest
|
||||
|
||||
private PlateOption FindSmallestFittingOption(Box partBounds)
|
||||
{
|
||||
return _sortedOptions?.FirstOrDefault(o => FitsBounds(OptionWorkArea(o, _template), partBounds));
|
||||
return _sortedOptions?.FirstOrDefault(o =>
|
||||
FitsBounds(OptionWorkArea(o, _template), partBounds)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Orchestration ---
|
||||
|
||||
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
||||
private MultiPlateResult Run(
|
||||
List<NestItem> items,
|
||||
PartSortOrder sortOrder,
|
||||
bool allowPlateCreation
|
||||
)
|
||||
{
|
||||
var result = new MultiPlateResult();
|
||||
|
||||
@@ -459,9 +479,10 @@ namespace OpenNest
|
||||
var workArea = pr.Plate.WorkArea();
|
||||
var classification = Classify(partBounds, workArea);
|
||||
|
||||
remnantCache[pr] = classification == PartClass.Small
|
||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||
remnantCache[pr] =
|
||||
classification == PartClass.Small
|
||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||
}
|
||||
|
||||
foreach (var zone in remnantCache[pr])
|
||||
@@ -522,7 +543,9 @@ namespace OpenNest
|
||||
{
|
||||
var currentOption = pr.ChosenSize;
|
||||
var currentIdx = _sortedOptions.FindIndex(o =>
|
||||
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
||||
o.Width.IsEqualTo(currentOption.Width)
|
||||
&& o.Length.IsEqualTo(currentOption.Length)
|
||||
);
|
||||
|
||||
if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1)
|
||||
continue;
|
||||
@@ -531,8 +554,10 @@ namespace OpenNest
|
||||
{
|
||||
var upgradeOption = _sortedOptions[i];
|
||||
|
||||
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
||||
if (
|
||||
upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon
|
||||
)
|
||||
continue;
|
||||
|
||||
var smallestNew = FindSmallestFittingOption(partBounds);
|
||||
@@ -541,20 +566,29 @@ namespace OpenNest
|
||||
continue;
|
||||
|
||||
var utilEst = pr.Plate.Utilization();
|
||||
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
|
||||
_salvageRate, utilEst);
|
||||
var decision = EvaluateUpgradeVsNew(
|
||||
currentOption,
|
||||
upgradeOption,
|
||||
smallestNew,
|
||||
_salvageRate,
|
||||
utilEst
|
||||
);
|
||||
|
||||
if (decision.ShouldUpgrade)
|
||||
{
|
||||
var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
|
||||
{
|
||||
foreach (var remnant in remnants)
|
||||
var placed = TryWithUpgradedSize(
|
||||
pr,
|
||||
upgradeOption,
|
||||
remnants =>
|
||||
{
|
||||
if (FillAndPlace(pr, remnant, item) > 0)
|
||||
return true;
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
if (FillAndPlace(pr, remnant, item) > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
);
|
||||
|
||||
if (placed)
|
||||
return true;
|
||||
@@ -593,53 +627,69 @@ namespace OpenNest
|
||||
|
||||
var currentOption = target.ChosenSize;
|
||||
|
||||
foreach (var upgradeOption in _sortedOptions.Where(o =>
|
||||
o.Width >= currentOption.Width - Tolerance.Epsilon
|
||||
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
||||
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
||||
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
||||
foreach (
|
||||
var upgradeOption in _sortedOptions.Where(o =>
|
||||
o.Width >= currentOption.Width - Tolerance.Epsilon
|
||||
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
||||
&& (
|
||||
o.Width > currentOption.Width + Tolerance.Epsilon
|
||||
|| o.Length > currentOption.Length + Tolerance.Epsilon
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
absorbed = TryWithUpgradedSize(target, upgradeOption, remnants =>
|
||||
{
|
||||
var engine = NestEngineRegistry.Create(target.Plate);
|
||||
var tempItems = donorParts
|
||||
.GroupBy(p => p.BaseDrawing)
|
||||
.Select(g => new NestItem
|
||||
{
|
||||
Drawing = g.Key,
|
||||
Quantity = g.Count(),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var totalPlaced = new List<Part>();
|
||||
foreach (var remnant in remnants)
|
||||
absorbed = TryWithUpgradedSize(
|
||||
target,
|
||||
upgradeOption,
|
||||
remnants =>
|
||||
{
|
||||
var placed = engine.PackArea(remnant, tempItems, _progress, _token);
|
||||
totalPlaced.AddRange(placed);
|
||||
var engine = NestEngineRegistry.Create(target.Plate);
|
||||
var tempItems = donorParts
|
||||
.GroupBy(p => p.BaseDrawing)
|
||||
.Select(g => new NestItem
|
||||
{
|
||||
Drawing = g.Key,
|
||||
Quantity = g.Count(),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var ti in tempItems)
|
||||
var totalPlaced = new List<Part>();
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
var count = placed.Count(p => p.BaseDrawing == ti.Drawing);
|
||||
ti.Quantity = System.Math.Max(0, ti.Quantity - count);
|
||||
var placed = engine.PackArea(
|
||||
remnant,
|
||||
tempItems,
|
||||
_progress,
|
||||
_token
|
||||
);
|
||||
totalPlaced.AddRange(placed);
|
||||
|
||||
foreach (var ti in tempItems)
|
||||
{
|
||||
var count = placed.Count(p =>
|
||||
p.BaseDrawing == ti.Drawing
|
||||
);
|
||||
ti.Quantity = System.Math.Max(0, ti.Quantity - count);
|
||||
}
|
||||
|
||||
if (tempItems.All(ti => ti.Quantity <= 0))
|
||||
break;
|
||||
}
|
||||
|
||||
if (tempItems.All(ti => ti.Quantity <= 0))
|
||||
break;
|
||||
if (totalPlaced.Count >= donorParts.Count)
|
||||
{
|
||||
target.AddParts(totalPlaced);
|
||||
|
||||
foreach (var p in donorParts)
|
||||
donor.Plate.Parts.Remove(p);
|
||||
donor.Parts.Clear();
|
||||
_platePool.Remove(donor);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (totalPlaced.Count >= donorParts.Count)
|
||||
{
|
||||
target.AddParts(totalPlaced);
|
||||
|
||||
foreach (var p in donorParts)
|
||||
donor.Plate.Parts.Remove(p);
|
||||
donor.Parts.Clear();
|
||||
_platePool.Remove(donor);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
);
|
||||
|
||||
if (absorbed)
|
||||
break;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum NestDirection
|
||||
{
|
||||
Vertical,
|
||||
Horizontal
|
||||
Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -47,9 +47,17 @@ namespace OpenNest
|
||||
|
||||
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
|
||||
|
||||
public virtual List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
public virtual List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
)
|
||||
{
|
||||
return new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI };
|
||||
return new List<double>
|
||||
{
|
||||
classification.PrimaryAngle,
|
||||
classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI,
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void RecordProductiveAngles(List<AngleResult> angleResults) { }
|
||||
@@ -58,28 +66,43 @@ namespace OpenNest
|
||||
|
||||
// --- Virtual methods (side-effect-free, return parts) ---
|
||||
|
||||
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public virtual List<Part> Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
public virtual List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public virtual List<Part> Fill(
|
||||
List<Part> groupParts,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
public virtual List<Part> PackArea(Box box, List<NestItem> items,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public virtual List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
// --- Nest: multi-item strategy (virtual, side-effect-free) ---
|
||||
|
||||
public virtual List<Part> Nest(List<NestItem> items,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public virtual List<Part> Nest(
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new List<Part>();
|
||||
@@ -95,9 +118,7 @@ namespace OpenNest
|
||||
.ThenByDescending(i => i.Drawing.Area)
|
||||
.ToList();
|
||||
|
||||
var packItems = items
|
||||
.Where(i => !ShouldFill(i, plateArea))
|
||||
.ToList();
|
||||
var packItems = items.Where(i => !ShouldFill(i, plateArea)).ToList();
|
||||
|
||||
// Phase 1: Fill multi-quantity drawings using RemnantFiller.
|
||||
if (fillItems.Count > 0)
|
||||
@@ -117,12 +138,15 @@ namespace OpenNest
|
||||
foreach (var item in fillItems)
|
||||
{
|
||||
var placed = fillParts.Count(p =>
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing)
|
||||
);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||
}
|
||||
|
||||
// Update workArea for pack phase
|
||||
var placedObstacles = fillParts.Select(p => p.BoundingBox.Offset(Plate.PartSpacing)).ToList();
|
||||
var placedObstacles = fillParts
|
||||
.Select(p => p.BoundingBox.Offset(Plate.PartSpacing))
|
||||
.ToList();
|
||||
var finder = new RemnantFinder(workArea, placedObstacles);
|
||||
var remnants = finder.FindRemnants();
|
||||
if (remnants.Count > 0)
|
||||
@@ -138,8 +162,12 @@ namespace OpenNest
|
||||
var pairItems = packItems.Where(i => i.Quantity == 2).ToList();
|
||||
var regularPackItems = packItems.Where(i => i.Quantity != 2).ToList();
|
||||
|
||||
if (regularPackItems.Count > 0 && workArea.Width > 0 && workArea.Length > 0
|
||||
&& !token.IsCancellationRequested)
|
||||
if (
|
||||
regularPackItems.Count > 0
|
||||
&& workArea.Width > 0
|
||||
&& workArea.Length > 0
|
||||
&& !token.IsCancellationRequested
|
||||
)
|
||||
{
|
||||
var packParts = PackArea(workArea, regularPackItems, progress, token);
|
||||
|
||||
@@ -151,7 +179,8 @@ namespace OpenNest
|
||||
foreach (var item in regularPackItems)
|
||||
{
|
||||
var placed = packParts.Count(p =>
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing)
|
||||
);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||
}
|
||||
}
|
||||
@@ -172,8 +201,12 @@ namespace OpenNest
|
||||
|
||||
// --- FillExact (non-virtual, delegates to virtual Fill) ---
|
||||
|
||||
public List<Part> FillExact(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
public List<Part> FillExact(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return Fill(item, workArea, progress, token);
|
||||
}
|
||||
@@ -226,8 +259,7 @@ namespace OpenNest
|
||||
|
||||
// --- Protected utilities ---
|
||||
|
||||
internal static void ReportProgress(
|
||||
IProgress<NestProgress> progress, ProgressReport report)
|
||||
internal static void ReportProgress(IProgress<NestProgress> progress, ProgressReport report)
|
||||
{
|
||||
if (progress == null || report.Parts == null || report.Parts.Count == 0)
|
||||
return;
|
||||
@@ -236,18 +268,22 @@ namespace OpenNest
|
||||
foreach (var part in report.Parts)
|
||||
clonedParts.Add((Part)part.Clone());
|
||||
|
||||
Debug.WriteLine($"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, " +
|
||||
$"Parts={clonedParts.Count} | {report.Description}");
|
||||
Debug.WriteLine(
|
||||
$"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, "
|
||||
+ $"Parts={clonedParts.Count} | {report.Description}"
|
||||
);
|
||||
|
||||
progress.Report(new NestProgress
|
||||
{
|
||||
Phase = report.Phase,
|
||||
PlateNumber = report.PlateNumber,
|
||||
BestParts = clonedParts,
|
||||
Description = report.Description,
|
||||
ActiveWorkArea = report.WorkArea,
|
||||
IsOverallBest = report.IsOverallBest,
|
||||
});
|
||||
progress.Report(
|
||||
new NestProgress
|
||||
{
|
||||
Phase = report.Phase,
|
||||
PlateNumber = report.PlateNumber,
|
||||
BestParts = clonedParts,
|
||||
Description = report.Description,
|
||||
ActiveWorkArea = report.WorkArea,
|
||||
IsOverallBest = report.IsOverallBest,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected string BuildProgressSummary()
|
||||
@@ -263,14 +299,20 @@ namespace OpenNest
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
|
||||
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||
=> Comparer.IsBetter(candidate, current, workArea);
|
||||
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
|
||||
Comparer.IsBetter(candidate, current, workArea);
|
||||
|
||||
protected bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||
{
|
||||
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
|
||||
if (
|
||||
candidate != null
|
||||
&& candidate.Count > 0
|
||||
&& HasOverlaps(candidate, Plate.PartSpacing)
|
||||
)
|
||||
{
|
||||
Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
|
||||
Debug.WriteLine(
|
||||
$"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -290,10 +332,12 @@ namespace OpenNest
|
||||
{
|
||||
var box2 = parts[j].BoundingBox;
|
||||
|
||||
var overlapX = System.Math.Min(box1.Right, box2.Right)
|
||||
- System.Math.Max(box1.Left, box2.Left);
|
||||
var overlapY = System.Math.Min(box1.Top, box2.Top)
|
||||
- System.Math.Max(box1.Bottom, box2.Bottom);
|
||||
var overlapX =
|
||||
System.Math.Min(box1.Right, box2.Right)
|
||||
- System.Math.Max(box1.Left, box2.Left);
|
||||
var overlapY =
|
||||
System.Math.Min(box1.Top, box2.Top)
|
||||
- System.Math.Max(box1.Bottom, box2.Bottom);
|
||||
|
||||
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
|
||||
continue;
|
||||
@@ -304,9 +348,11 @@ namespace OpenNest
|
||||
{
|
||||
var b1 = parts[i].BoundingBox;
|
||||
var b2 = parts[j].BoundingBox;
|
||||
Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
|
||||
$" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
|
||||
$" intersections={pts?.Count ?? 0}");
|
||||
Debug.WriteLine(
|
||||
$"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}"
|
||||
+ $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}"
|
||||
+ $" intersections={pts?.Count ?? 0}"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -319,8 +365,11 @@ namespace OpenNest
|
||||
/// Places best-fit pairs for qty=2 items into remnant spaces around
|
||||
/// already-placed parts. Returns all placed pair parts.
|
||||
/// </summary>
|
||||
private List<Part> PlaceBestFitPairs(List<NestItem> pairItems,
|
||||
List<Part> existingParts, Box fullWorkArea)
|
||||
private List<Part> PlaceBestFitPairs(
|
||||
List<NestItem> pairItems,
|
||||
List<Part> existingParts,
|
||||
Box fullWorkArea
|
||||
)
|
||||
{
|
||||
var result = new List<Part>();
|
||||
var obstacles = existingParts
|
||||
@@ -330,10 +379,15 @@ namespace OpenNest
|
||||
|
||||
foreach (var item in pairItems)
|
||||
{
|
||||
if (item.Quantity < 2) continue;
|
||||
if (item.Quantity < 2)
|
||||
continue;
|
||||
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
item.Drawing,
|
||||
Plate.Size.Length,
|
||||
Plate.Size.Width,
|
||||
Plate.PartSpacing
|
||||
);
|
||||
|
||||
// BestFitCache stores pair coordinates in canonical frame. Build candidates
|
||||
// from a canonical drawing copy so geometry and coords share a frame; rebind
|
||||
@@ -359,8 +413,10 @@ namespace OpenNest
|
||||
|
||||
foreach (var r in remnants)
|
||||
{
|
||||
if (pairW <= r.Width + Tolerance.Epsilon &&
|
||||
pairL <= r.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
pairW <= r.Width + Tolerance.Epsilon
|
||||
&& pairL <= r.Length + Tolerance.Epsilon
|
||||
)
|
||||
{
|
||||
var offset = r.Location - pairBbox.Location;
|
||||
foreach (var p in parts)
|
||||
@@ -379,7 +435,8 @@ namespace OpenNest
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement == null) continue;
|
||||
if (bestPlacement == null)
|
||||
continue;
|
||||
|
||||
// Rebind to the original drawing and compose sourceAngle onto rotation so the
|
||||
// final placed parts sit in the user's visible frame.
|
||||
@@ -391,9 +448,11 @@ namespace OpenNest
|
||||
var envelope = ((IEnumerable<IBoundable>)bestPlacement).GetBoundingBox();
|
||||
finder.AddObstacle(envelope.Offset(Plate.PartSpacing));
|
||||
|
||||
Debug.WriteLine($"[Nest] Placed best-fit pair for {item.Drawing.Name} " +
|
||||
$"at ({bestTarget.X:F1},{bestTarget.Y:F1}), " +
|
||||
$"size {envelope.Width:F1}x{envelope.Length:F1}");
|
||||
Debug.WriteLine(
|
||||
$"[Nest] Placed best-fit pair for {item.Drawing.Name} "
|
||||
+ $"at ({bestTarget.X:F1},{bestTarget.Y:F1}), "
|
||||
+ $"size {envelope.Width:F1}x{envelope.Length:F1}"
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -405,7 +464,11 @@ namespace OpenNest
|
||||
/// the returned list is in the original drawing's visible frame. Mirrors
|
||||
/// DefaultNestEngine.RebindAndUnCanonicalize.
|
||||
/// </summary>
|
||||
private static List<Part> RebindPairToOriginal(List<Part> parts, Drawing original, double sourceAngle)
|
||||
private static List<Part> RebindPairToOriginal(
|
||||
List<Part> parts,
|
||||
Drawing original,
|
||||
double sourceAngle
|
||||
)
|
||||
{
|
||||
if (parts == null || parts.Count == 0)
|
||||
return parts;
|
||||
@@ -444,6 +507,5 @@ namespace OpenNest
|
||||
// packing produces better results than grid-filling.
|
||||
return totalArea >= plateArea * 0.1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,29 @@ namespace OpenNest
|
||||
|
||||
static NestEngineRegistry()
|
||||
{
|
||||
Register("Default",
|
||||
Register(
|
||||
"Default",
|
||||
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||
plate => new DefaultNestEngine(plate));
|
||||
plate => new DefaultNestEngine(plate)
|
||||
);
|
||||
|
||||
Register("Strip",
|
||||
Register(
|
||||
"Strip",
|
||||
"Strip-based nesting for mixed-drawing layouts",
|
||||
plate => new StripNestEngine(plate));
|
||||
plate => new StripNestEngine(plate)
|
||||
);
|
||||
|
||||
Register("Vertical Remnant",
|
||||
Register(
|
||||
"Vertical Remnant",
|
||||
"Optimizes for largest right-side vertical drop",
|
||||
plate => new VerticalRemnantEngine(plate));
|
||||
plate => new VerticalRemnantEngine(plate)
|
||||
);
|
||||
|
||||
Register("Horizontal Remnant",
|
||||
Register(
|
||||
"Horizontal Remnant",
|
||||
"Optimizes for largest top-side horizontal drop",
|
||||
plate => new HorizontalRemnantEngine(plate));
|
||||
plate => new HorizontalRemnantEngine(plate)
|
||||
);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<NestEngineInfo> AvailableEngines => engines;
|
||||
@@ -37,18 +45,25 @@ namespace OpenNest
|
||||
public static NestEngineBase Create(Plate plate)
|
||||
{
|
||||
var info = engines.FirstOrDefault(e =>
|
||||
e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase));
|
||||
e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
Debug.WriteLine($"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default");
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default"
|
||||
);
|
||||
info = engines[0];
|
||||
}
|
||||
|
||||
return info.Factory(plate);
|
||||
}
|
||||
|
||||
public static void Register(string name, string description, Func<Plate, NestEngineBase> factory)
|
||||
public static void Register(
|
||||
string name,
|
||||
string description,
|
||||
Func<Plate, NestEngineBase> factory
|
||||
)
|
||||
{
|
||||
if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
@@ -79,7 +94,9 @@ namespace OpenNest
|
||||
|
||||
if (ctor == null)
|
||||
{
|
||||
Debug.WriteLine($"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor");
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -88,19 +105,28 @@ namespace OpenNest
|
||||
{
|
||||
var tempPlate = new Plate();
|
||||
var instance = (NestEngineBase)ctor.Invoke(new object[] { tempPlate });
|
||||
Register(instance.Name, instance.Description,
|
||||
plate => (NestEngineBase)ctor.Invoke(new object[] { plate }));
|
||||
Debug.WriteLine($"[NestEngineRegistry] Loaded plugin engine: {instance.Name}");
|
||||
Register(
|
||||
instance.Name,
|
||||
instance.Description,
|
||||
plate => (NestEngineBase)ctor.Invoke(new object[] { plate })
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Loaded plugin engine: {instance.Name}"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}");
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}");
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -15,12 +15,23 @@ namespace OpenNest
|
||||
|
||||
public enum NestPhase
|
||||
{
|
||||
[Description("Trying rotations..."), ShortName("Linear")] Linear,
|
||||
[Description("Trying best fit..."), ShortName("BestFit")] RectBestFit,
|
||||
[Description("Trying pairs..."), ShortName("Pairs")] Pairs,
|
||||
[Description("Trying NFP..."), ShortName("NFP")] Nfp,
|
||||
[Description("Trying extents..."), ShortName("Extents")] Extents,
|
||||
[Description("Custom"), ShortName("Custom")] Custom
|
||||
[Description("Trying rotations..."), ShortName("Linear")]
|
||||
Linear,
|
||||
|
||||
[Description("Trying best fit..."), ShortName("BestFit")]
|
||||
RectBestFit,
|
||||
|
||||
[Description("Trying pairs..."), ShortName("Pairs")]
|
||||
Pairs,
|
||||
|
||||
[Description("Trying NFP..."), ShortName("NFP")]
|
||||
Nfp,
|
||||
|
||||
[Description("Trying extents..."), ShortName("Extents")]
|
||||
Extents,
|
||||
|
||||
[Description("Custom"), ShortName("Custom")]
|
||||
Custom,
|
||||
}
|
||||
|
||||
public static class NestPhaseExtensions
|
||||
@@ -30,22 +41,28 @@ namespace OpenNest
|
||||
|
||||
public static string DisplayName(this NestPhase phase)
|
||||
{
|
||||
return DisplayNames.GetOrAdd(phase, p =>
|
||||
{
|
||||
var field = typeof(NestPhase).GetField(p.ToString());
|
||||
var attr = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attr?.Description ?? p.ToString();
|
||||
});
|
||||
return DisplayNames.GetOrAdd(
|
||||
phase,
|
||||
p =>
|
||||
{
|
||||
var field = typeof(NestPhase).GetField(p.ToString());
|
||||
var attr = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attr?.Description ?? p.ToString();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static string ShortName(this NestPhase phase)
|
||||
{
|
||||
return ShortNames.GetOrAdd(phase, p =>
|
||||
{
|
||||
var field = typeof(NestPhase).GetField(p.ToString());
|
||||
var attr = field?.GetCustomAttribute<ShortNameAttribute>();
|
||||
return attr?.Name ?? p.ToString();
|
||||
});
|
||||
return ShortNames.GetOrAdd(
|
||||
phase,
|
||||
p =>
|
||||
{
|
||||
var field = typeof(NestPhase).GetField(p.ToString());
|
||||
var attr = field?.GetCustomAttribute<ShortNameAttribute>();
|
||||
return attr?.Name ?? p.ToString();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +106,11 @@ namespace OpenNest
|
||||
public List<Part> BestParts
|
||||
{
|
||||
get => bestParts;
|
||||
set { bestParts = value; cachedParts = null; }
|
||||
set
|
||||
{
|
||||
bestParts = value;
|
||||
cachedParts = null;
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get; set; }
|
||||
@@ -104,7 +125,8 @@ namespace OpenNest
|
||||
|
||||
private void EnsureCache()
|
||||
{
|
||||
if (cachedParts == bestParts) return;
|
||||
if (cachedParts == bestParts)
|
||||
return;
|
||||
cachedParts = bestParts;
|
||||
if (bestParts == null || bestParts.Count == 0)
|
||||
{
|
||||
@@ -122,7 +144,8 @@ namespace OpenNest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BestParts == null || BestParts.Count == 0) return 0;
|
||||
if (BestParts == null || BestParts.Count == 0)
|
||||
return 0;
|
||||
EnsureCache();
|
||||
var bboxArea = cachedBounds.Width * cachedBounds.Length;
|
||||
return bboxArea > 0 ? cachedPartArea / bboxArea : 0;
|
||||
@@ -133,7 +156,8 @@ namespace OpenNest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BestParts == null || BestParts.Count == 0) return 0;
|
||||
if (BestParts == null || BestParts.Count == 0)
|
||||
return 0;
|
||||
EnsureCache();
|
||||
return cachedBounds.Width;
|
||||
}
|
||||
@@ -143,7 +167,8 @@ namespace OpenNest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BestParts == null || BestParts.Count == 0) return 0;
|
||||
if (BestParts == null || BestParts.Count == 0)
|
||||
return 0;
|
||||
EnsureCache();
|
||||
return cachedBounds.Length;
|
||||
}
|
||||
@@ -153,7 +178,8 @@ namespace OpenNest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BestParts == null || BestParts.Count == 0) return 0;
|
||||
if (BestParts == null || BestParts.Count == 0)
|
||||
return 0;
|
||||
EnsureCache();
|
||||
return cachedPartArea;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
@@ -15,9 +15,12 @@ namespace OpenNest.Engine.Nfp
|
||||
/// </summary>
|
||||
public static class AutoNester
|
||||
{
|
||||
public static List<Part> Nest(List<NestItem> items, Plate plate,
|
||||
public static List<Part> Nest(
|
||||
List<NestItem> items,
|
||||
Plate plate,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken cancellation = default)
|
||||
CancellationToken cancellation = default
|
||||
)
|
||||
{
|
||||
var workArea = plate.WorkArea();
|
||||
var halfSpacing = plate.PartSpacing / 2.0;
|
||||
@@ -36,7 +39,9 @@ namespace OpenNest.Engine.Nfp
|
||||
|
||||
if (perimeterPolygon == null)
|
||||
{
|
||||
Debug.WriteLine($"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -58,11 +63,20 @@ namespace OpenNest.Engine.Nfp
|
||||
// Pre-compute all NFPs.
|
||||
nfpCache.PreComputeAll();
|
||||
|
||||
Debug.WriteLine($"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings"
|
||||
);
|
||||
|
||||
// Run simulated annealing optimizer.
|
||||
var optimizer = new SimulatedAnnealing();
|
||||
var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, progress, cancellation);
|
||||
var result = optimizer.Optimize(
|
||||
items,
|
||||
workArea,
|
||||
nfpCache,
|
||||
candidateRotations,
|
||||
progress,
|
||||
cancellation
|
||||
);
|
||||
|
||||
if (result.Sequence == null || result.Sequence.Count == 0)
|
||||
return new List<Part>();
|
||||
@@ -72,17 +86,22 @@ namespace OpenNest.Engine.Nfp
|
||||
var placedParts = blf.Fill(result.Sequence);
|
||||
var parts = BottomLeftFill.ToNestParts(placedParts);
|
||||
|
||||
Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations"
|
||||
);
|
||||
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Nfp,
|
||||
PlateNumber = 0,
|
||||
Parts = parts,
|
||||
WorkArea = workArea,
|
||||
Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations",
|
||||
IsOverallBest = true,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Nfp,
|
||||
PlateNumber = 0,
|
||||
Parts = parts,
|
||||
WorkArea = workArea,
|
||||
Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations",
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
|
||||
return parts;
|
||||
}
|
||||
@@ -147,7 +166,9 @@ namespace OpenNest.Engine.Nfp
|
||||
// Only use the NFP result if it kept all parts and improved density.
|
||||
if (optimized.Count < parts.Count)
|
||||
{
|
||||
Debug.WriteLine($"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts"
|
||||
);
|
||||
return parts;
|
||||
}
|
||||
|
||||
@@ -163,24 +184,32 @@ namespace OpenNest.Engine.Nfp
|
||||
|
||||
if (optimizedScore > originalScore)
|
||||
{
|
||||
Debug.WriteLine($"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}"
|
||||
);
|
||||
return optimized;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}");
|
||||
Debug.WriteLine(
|
||||
$"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}"
|
||||
);
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static bool AllPartsInBounds(List<Part> parts, Box workArea)
|
||||
{
|
||||
var logPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log");
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
|
||||
"nest-debug.log"
|
||||
);
|
||||
|
||||
var allInBounds = true;
|
||||
|
||||
// Append to the log that BLF already started
|
||||
using var log = new StreamWriter(logPath, true);
|
||||
log.WriteLine($"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}");
|
||||
log.WriteLine(
|
||||
$"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"
|
||||
);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
@@ -193,7 +222,9 @@ namespace OpenNest.Engine.Nfp
|
||||
|
||||
if (oob)
|
||||
{
|
||||
log.WriteLine($"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}");
|
||||
log.WriteLine(
|
||||
$"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}"
|
||||
);
|
||||
allInBounds = false;
|
||||
}
|
||||
}
|
||||
@@ -215,8 +246,11 @@ namespace OpenNest.Engine.Nfp
|
||||
/// <summary>
|
||||
/// Computes candidate rotation angles for a drawing.
|
||||
/// </summary>
|
||||
private static List<double> ComputeCandidateRotations(NestItem item,
|
||||
Polygon perimeterPolygon, Box workArea)
|
||||
private static List<double> ComputeCandidateRotations(
|
||||
NestItem item,
|
||||
Polygon perimeterPolygon,
|
||||
Box workArea
|
||||
)
|
||||
{
|
||||
var rotations = new List<double> { 0 };
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
@@ -14,7 +14,9 @@ namespace OpenNest.Engine.Nfp
|
||||
public class BottomLeftFill
|
||||
{
|
||||
private static readonly string DebugLogPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log");
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
|
||||
"nest-debug.log"
|
||||
);
|
||||
|
||||
private readonly Box workArea;
|
||||
private readonly NfpCache nfpCache;
|
||||
@@ -34,7 +36,9 @@ namespace OpenNest.Engine.Nfp
|
||||
var placedParts = new List<PlacedPart>();
|
||||
|
||||
using var log = new StreamWriter(DebugLogPath, false);
|
||||
log.WriteLine($"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}");
|
||||
log.WriteLine(
|
||||
$"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"
|
||||
);
|
||||
log.WriteLine($"[BLF] Sequence count: {sequence.Count}");
|
||||
|
||||
foreach (var entry in sequence)
|
||||
@@ -43,13 +47,22 @@ namespace OpenNest.Engine.Nfp
|
||||
|
||||
if (ifp.Vertices.Count < 3)
|
||||
{
|
||||
log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)");
|
||||
log.WriteLine(
|
||||
$"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})");
|
||||
log.WriteLine(
|
||||
$"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})"
|
||||
);
|
||||
|
||||
var nfpPaths = ComputeNfpPaths(placedParts, entry.DrawingId, entry.Rotation, ifp.BoundingBox);
|
||||
var nfpPaths = ComputeNfpPaths(
|
||||
placedParts,
|
||||
entry.DrawingId,
|
||||
entry.Rotation,
|
||||
ifp.BoundingBox
|
||||
);
|
||||
var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfpPaths);
|
||||
var point = InnerFitPolygon.FindBottomLeftPoint(feasible);
|
||||
|
||||
@@ -63,17 +76,22 @@ namespace OpenNest.Engine.Nfp
|
||||
var ifpBb = ifp.BoundingBox;
|
||||
point = new Vector(
|
||||
System.Math.Max(ifpBb.X, System.Math.Min(ifpBb.Right, point.X)),
|
||||
System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y)));
|
||||
System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y))
|
||||
);
|
||||
|
||||
log.WriteLine($"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}");
|
||||
log.WriteLine(
|
||||
$"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}"
|
||||
);
|
||||
|
||||
placedParts.Add(new PlacedPart
|
||||
{
|
||||
DrawingId = entry.DrawingId,
|
||||
Rotation = entry.Rotation,
|
||||
Position = point,
|
||||
Drawing = entry.Drawing
|
||||
});
|
||||
placedParts.Add(
|
||||
new PlacedPart
|
||||
{
|
||||
DrawingId = entry.DrawingId,
|
||||
Rotation = entry.Rotation,
|
||||
Position = point,
|
||||
Drawing = entry.Drawing,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}");
|
||||
@@ -106,7 +124,12 @@ namespace OpenNest.Engine.Nfp
|
||||
/// returned as Clipper paths with translations applied.
|
||||
/// Filters NFPs that don't intersect the target IFP.
|
||||
/// </summary>
|
||||
private PathsD ComputeNfpPaths(List<PlacedPart> placedParts, int drawingId, double rotation, Box ifpBounds)
|
||||
private PathsD ComputeNfpPaths(
|
||||
List<PlacedPart> placedParts,
|
||||
int drawingId,
|
||||
double rotation,
|
||||
Box ifpBounds
|
||||
)
|
||||
{
|
||||
var nfpPaths = new PathsD(placedParts.Count);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
@@ -33,9 +33,13 @@ namespace OpenNest.Engine.Nfp
|
||||
/// </summary>
|
||||
public interface INestOptimizer
|
||||
{
|
||||
OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
||||
OptimizationResult Optimize(
|
||||
List<NestItem> items,
|
||||
Box workArea,
|
||||
NfpCache cache,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken cancellation = default);
|
||||
CancellationToken cancellation = default
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
@@ -12,10 +12,10 @@ namespace OpenNest.Engine.Nfp
|
||||
public class NfpCache
|
||||
{
|
||||
private readonly Dictionary<NfpKey, Polygon> cache = new Dictionary<NfpKey, Polygon>();
|
||||
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache
|
||||
= new Dictionary<int, Dictionary<double, Polygon>>();
|
||||
private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache
|
||||
= new Dictionary<(int drawingId, double rotation), Polygon>();
|
||||
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache =
|
||||
new Dictionary<int, Dictionary<double, Polygon>>();
|
||||
private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache =
|
||||
new Dictionary<(int drawingId, double rotation), Polygon>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a pre-computed polygon for a drawing at a specific rotation.
|
||||
@@ -107,8 +107,12 @@ namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
for (var j = 0; j < entries.Count; j++)
|
||||
{
|
||||
Get(entries[i].drawingId, entries[i].rotation,
|
||||
entries[j].drawingId, entries[j].rotation);
|
||||
Get(
|
||||
entries[i].drawingId,
|
||||
entries[i].rotation,
|
||||
entries[j].drawingId,
|
||||
entries[j].rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,9 +140,9 @@ namespace OpenNest.Engine.Nfp
|
||||
public bool Equals(NfpKey other)
|
||||
{
|
||||
return DrawingIdA == other.DrawingIdA
|
||||
&& RotationA == other.RotationA
|
||||
&& DrawingIdB == other.DrawingIdB
|
||||
&& RotationB == other.RotationB;
|
||||
&& RotationA == other.RotationA
|
||||
&& DrawingIdB == other.DrawingIdB
|
||||
&& RotationB == other.RotationB;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) => obj is NfpKey key && Equals(key);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
@@ -18,10 +18,14 @@ namespace OpenNest.Engine.Nfp
|
||||
private const double DefaultMinTemperature = 0.1;
|
||||
private const int DefaultMaxNoImprovement = 500;
|
||||
|
||||
public OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
||||
public OptimizationResult Optimize(
|
||||
List<NestItem> items,
|
||||
Box workArea,
|
||||
NfpCache cache,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken cancellation = default)
|
||||
CancellationToken cancellation = default
|
||||
)
|
||||
{
|
||||
var random = new Random();
|
||||
|
||||
@@ -30,7 +34,12 @@ namespace OpenNest.Engine.Nfp
|
||||
var sequence = BuildInitialSequence(items, candidateRotations);
|
||||
|
||||
if (sequence.Count == 0)
|
||||
return new OptimizationResult { Sequence = sequence, Score = default, Iterations = 0 };
|
||||
return new OptimizationResult
|
||||
{
|
||||
Sequence = sequence,
|
||||
Score = default,
|
||||
Iterations = 0,
|
||||
};
|
||||
|
||||
// Evaluate initial solution.
|
||||
var blf = new BottomLeftFill(workArea, cache);
|
||||
@@ -42,20 +51,33 @@ namespace OpenNest.Engine.Nfp
|
||||
var currentScore = bestScore;
|
||||
|
||||
// Calibrate initial temperature so ~80% of worse moves are accepted.
|
||||
var initialTemp = CalibrateTemperature(currentSequence, workArea, cache,
|
||||
candidateRotations, random);
|
||||
var initialTemp = CalibrateTemperature(
|
||||
currentSequence,
|
||||
workArea,
|
||||
cache,
|
||||
candidateRotations,
|
||||
random
|
||||
);
|
||||
var temperature = initialTemp;
|
||||
var noImprovement = 0;
|
||||
var iteration = 0;
|
||||
|
||||
Debug.WriteLine($"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}"
|
||||
);
|
||||
|
||||
ReportBest(progress, BottomLeftFill.ToNestParts(bestPlaced), workArea,
|
||||
$"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
ReportBest(
|
||||
progress,
|
||||
BottomLeftFill.ToNestParts(bestPlaced),
|
||||
workArea,
|
||||
$"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}"
|
||||
);
|
||||
|
||||
while (temperature > DefaultMinTemperature
|
||||
&& noImprovement < DefaultMaxNoImprovement
|
||||
&& !cancellation.IsCancellationRequested)
|
||||
while (
|
||||
temperature > DefaultMinTemperature
|
||||
&& noImprovement < DefaultMaxNoImprovement
|
||||
&& !cancellation.IsCancellationRequested
|
||||
)
|
||||
{
|
||||
iteration++;
|
||||
|
||||
@@ -63,7 +85,10 @@ namespace OpenNest.Engine.Nfp
|
||||
Mutate(candidate, candidateRotations, random);
|
||||
|
||||
var candidatePlaced = blf.Fill(candidate);
|
||||
var candidateScore = FillScore.Compute(BottomLeftFill.ToNestParts(candidatePlaced), workArea);
|
||||
var candidateScore = FillScore.Compute(
|
||||
BottomLeftFill.ToNestParts(candidatePlaced),
|
||||
workArea
|
||||
);
|
||||
|
||||
var delta = candidateScore.CompareTo(currentScore);
|
||||
|
||||
@@ -79,10 +104,16 @@ namespace OpenNest.Engine.Nfp
|
||||
bestSequence = new List<SequenceEntry>(currentSequence);
|
||||
noImprovement = 0;
|
||||
|
||||
Debug.WriteLine($"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
Debug.WriteLine(
|
||||
$"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}"
|
||||
);
|
||||
|
||||
ReportBest(progress, BottomLeftFill.ToNestParts(candidatePlaced), workArea,
|
||||
$"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
ReportBest(
|
||||
progress,
|
||||
BottomLeftFill.ToNestParts(candidatePlaced),
|
||||
workArea,
|
||||
$"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -111,13 +142,15 @@ namespace OpenNest.Engine.Nfp
|
||||
temperature *= DefaultCoolingRate;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[SA] Done: {iteration} iters, best={bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
Debug.WriteLine(
|
||||
$"[SA] Done: {iteration} iters, best={bestScore.Count} parts, density={bestScore.Density:P1}"
|
||||
);
|
||||
|
||||
return new OptimizationResult
|
||||
{
|
||||
Sequence = bestSequence,
|
||||
Score = bestScore,
|
||||
Iterations = iteration
|
||||
Iterations = iteration,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,7 +159,9 @@ namespace OpenNest.Engine.Nfp
|
||||
/// Each NestItem is expanded by its quantity.
|
||||
/// </summary>
|
||||
private static List<SequenceEntry> BuildInitialSequence(
|
||||
List<NestItem> items, Dictionary<int, List<double>> candidateRotations)
|
||||
List<NestItem> items,
|
||||
Dictionary<int, List<double>> candidateRotations
|
||||
)
|
||||
{
|
||||
var sequence = new List<SequenceEntry>();
|
||||
|
||||
@@ -138,7 +173,10 @@ namespace OpenNest.Engine.Nfp
|
||||
var qty = item.Quantity > 0 ? item.Quantity : 1;
|
||||
var rotation = 0.0;
|
||||
|
||||
if (candidateRotations.TryGetValue(item.Drawing.Id, out var rotations) && rotations.Count > 0)
|
||||
if (
|
||||
candidateRotations.TryGetValue(item.Drawing.Id, out var rotations)
|
||||
&& rotations.Count > 0
|
||||
)
|
||||
rotation = rotations[0];
|
||||
|
||||
for (var i = 0; i < qty; i++)
|
||||
@@ -151,8 +189,11 @@ namespace OpenNest.Engine.Nfp
|
||||
/// <summary>
|
||||
/// Applies a random mutation to the sequence.
|
||||
/// </summary>
|
||||
private static void Mutate(List<SequenceEntry> sequence,
|
||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||
private static void Mutate(
|
||||
List<SequenceEntry> sequence,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
Random random
|
||||
)
|
||||
{
|
||||
if (sequence.Count < 2)
|
||||
return;
|
||||
@@ -190,13 +231,19 @@ namespace OpenNest.Engine.Nfp
|
||||
/// <summary>
|
||||
/// Changes a random part's rotation to another candidate angle.
|
||||
/// </summary>
|
||||
private static void MutateRotate(List<SequenceEntry> sequence,
|
||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||
private static void MutateRotate(
|
||||
List<SequenceEntry> sequence,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
Random random
|
||||
)
|
||||
{
|
||||
var idx = random.Next(sequence.Count);
|
||||
var entry = sequence[idx];
|
||||
|
||||
if (!candidateRotations.TryGetValue(entry.DrawingId, out var rotations) || rotations.Count <= 1)
|
||||
if (
|
||||
!candidateRotations.TryGetValue(entry.DrawingId, out var rotations)
|
||||
|| rotations.Count <= 1
|
||||
)
|
||||
return;
|
||||
|
||||
var newRotation = rotations[random.Next(rotations.Count)];
|
||||
@@ -229,8 +276,11 @@ namespace OpenNest.Engine.Nfp
|
||||
/// </summary>
|
||||
private static double CalibrateTemperature(
|
||||
List<SequenceEntry> sequence,
|
||||
Box workArea, NfpCache cache,
|
||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||
Box workArea,
|
||||
NfpCache cache,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
Random random
|
||||
)
|
||||
{
|
||||
const int samples = 20;
|
||||
var deltas = new List<double>();
|
||||
@@ -274,18 +324,25 @@ namespace OpenNest.Engine.Nfp
|
||||
return countDiff * 10.0 + densityDiff;
|
||||
}
|
||||
|
||||
private static void ReportBest(IProgress<NestProgress> progress, List<Part> parts,
|
||||
Box workArea, string description)
|
||||
private static void ReportBest(
|
||||
IProgress<NestProgress> progress,
|
||||
List<Part> parts,
|
||||
Box workArea,
|
||||
string description
|
||||
)
|
||||
{
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Nfp,
|
||||
PlateNumber = 0,
|
||||
Parts = parts,
|
||||
WorkArea = workArea,
|
||||
Description = description,
|
||||
IsOverallBest = true,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Nfp,
|
||||
PlateNumber = 0,
|
||||
Parts = parts,
|
||||
WorkArea = workArea,
|
||||
Description = description,
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public enum PartType { Rectangle, Circle, Irregular }
|
||||
public enum PartType
|
||||
{
|
||||
Rectangle,
|
||||
Circle,
|
||||
Irregular,
|
||||
}
|
||||
|
||||
public struct ClassificationResult
|
||||
{
|
||||
@@ -27,7 +32,8 @@ namespace OpenNest.Engine
|
||||
{
|
||||
var result = new ClassificationResult { Type = PartType.Irregular };
|
||||
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
@@ -72,7 +78,8 @@ namespace OpenNest.Engine
|
||||
|
||||
// Circularity: 4*PI*area / perimeter^2. Circles ~ 1.0.
|
||||
if (drawingPerimeter > Tolerance.Epsilon)
|
||||
result.Circularity = 4 * System.Math.PI * perimeterArea / (drawingPerimeter * drawingPerimeter);
|
||||
result.Circularity =
|
||||
4 * System.Math.PI * perimeterArea / (drawingPerimeter * drawingPerimeter);
|
||||
|
||||
// Check circle first (rotationally invariant).
|
||||
if (result.Circularity >= CircularityThreshold)
|
||||
@@ -90,8 +97,10 @@ namespace OpenNest.Engine
|
||||
result.PerimeterRatio = mbrPerimeter / drawingPerimeter;
|
||||
|
||||
// Rectangle: both metrics pass thresholds.
|
||||
if (result.Rectangularity >= RectangularityThreshold
|
||||
&& result.PerimeterRatio >= PerimeterRatioThreshold)
|
||||
if (
|
||||
result.Rectangularity >= RectangularityThreshold
|
||||
&& result.PerimeterRatio >= PerimeterRatioThreshold
|
||||
)
|
||||
{
|
||||
result.Type = PartType.Rectangle;
|
||||
return result;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -18,9 +18,15 @@ namespace OpenNest
|
||||
double salvageRate,
|
||||
Plate templatePlate,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0 || plateOptions == null || plateOptions.Count == 0)
|
||||
if (
|
||||
items == null
|
||||
|| items.Count == 0
|
||||
|| plateOptions == null
|
||||
|| plateOptions.Count == 0
|
||||
)
|
||||
return null;
|
||||
|
||||
// Find the minimum dimension needed to fit the largest part,
|
||||
@@ -29,20 +35,29 @@ namespace OpenNest
|
||||
var minPartLength = 0.0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Quantity <= 0) continue;
|
||||
if (item.Quantity <= 0)
|
||||
continue;
|
||||
var bb = item.Drawing.Program.BoundingBox();
|
||||
var shortSide = System.Math.Min(bb.Width, bb.Length);
|
||||
var longSide = System.Math.Max(bb.Width, bb.Length);
|
||||
|
||||
if (!plateOptions.Any(o => FitsPart(o, shortSide, longSide, templatePlate.EdgeSpacing)))
|
||||
if (
|
||||
!plateOptions.Any(o =>
|
||||
FitsPart(o, shortSide, longSide, templatePlate.EdgeSpacing)
|
||||
)
|
||||
)
|
||||
{
|
||||
Debug.WriteLine($"[PlateOptimizer] Skipping oversized item '{item.Drawing.Name}' " +
|
||||
$"({shortSide:F1}x{longSide:F1}) — does not fit any plate option");
|
||||
Debug.WriteLine(
|
||||
$"[PlateOptimizer] Skipping oversized item '{item.Drawing.Name}' "
|
||||
+ $"({shortSide:F1}x{longSide:F1}) — does not fit any plate option"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shortSide > minPartWidth) minPartWidth = shortSide;
|
||||
if (longSide > minPartLength) minPartLength = longSide;
|
||||
if (shortSide > minPartWidth)
|
||||
minPartWidth = shortSide;
|
||||
if (longSide > minPartLength)
|
||||
minPartLength = longSide;
|
||||
}
|
||||
|
||||
// Sort candidates by cost ascending — try cheapest first.
|
||||
@@ -57,13 +72,12 @@ namespace OpenNest
|
||||
// Pre-compute best fits for all candidate plate sizes at once.
|
||||
// This runs the expensive GPU evaluation once on the largest plate
|
||||
// and filters the results for each smaller size.
|
||||
var plateSizes = candidates
|
||||
.Select(o => (Width: o.Length, Height: o.Width))
|
||||
.ToList();
|
||||
var plateSizes = candidates.Select(o => (Width: o.Length, Height: o.Width)).ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Quantity <= 0) continue;
|
||||
if (item.Quantity <= 0)
|
||||
continue;
|
||||
BestFitCache.ComputeForSizes(item.Drawing, templatePlate.PartSpacing, plateSizes);
|
||||
}
|
||||
|
||||
@@ -74,7 +88,14 @@ namespace OpenNest
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
var result = TryPlateSize(option, items, salvageRate, templatePlate, progress, token);
|
||||
var result = TryPlateSize(
|
||||
option,
|
||||
items,
|
||||
salvageRate,
|
||||
templatePlate,
|
||||
progress,
|
||||
token
|
||||
);
|
||||
if (result == null)
|
||||
continue;
|
||||
|
||||
@@ -86,11 +107,16 @@ namespace OpenNest
|
||||
// remnant credit never offsets the extra plate cost, so skip.
|
||||
if (salvageRate < 1.0)
|
||||
{
|
||||
var allPlaced = items.All(i => i.Quantity <= 0 ||
|
||||
result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity);
|
||||
var allPlaced = items.All(i =>
|
||||
i.Quantity <= 0
|
||||
|| result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name)
|
||||
>= i.Quantity
|
||||
);
|
||||
if (allPlaced)
|
||||
{
|
||||
Debug.WriteLine($"[PlateOptimizer] Early exit: {option.Width}x{option.Length} placed all items");
|
||||
Debug.WriteLine(
|
||||
$"[PlateOptimizer] Early exit: {option.Width}x{option.Length} placed all items"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -99,14 +125,21 @@ namespace OpenNest
|
||||
return best;
|
||||
}
|
||||
|
||||
private static bool FitsPart(PlateOption option, double minWidth, double minLength, Spacing edgeSpacing)
|
||||
private static bool FitsPart(
|
||||
PlateOption option,
|
||||
double minWidth,
|
||||
double minLength,
|
||||
Spacing edgeSpacing
|
||||
)
|
||||
{
|
||||
var workW = option.Width - edgeSpacing.Left - edgeSpacing.Right;
|
||||
var workL = option.Length - edgeSpacing.Top - edgeSpacing.Bottom;
|
||||
|
||||
// Part fits in either orientation.
|
||||
var fitsNormal = workW >= minWidth - Tolerance.Epsilon && workL >= minLength - Tolerance.Epsilon;
|
||||
var fitsRotated = workW >= minLength - Tolerance.Epsilon && workL >= minWidth - Tolerance.Epsilon;
|
||||
var fitsNormal =
|
||||
workW >= minWidth - Tolerance.Epsilon && workL >= minLength - Tolerance.Epsilon;
|
||||
var fitsRotated =
|
||||
workW >= minLength - Tolerance.Epsilon && workL >= minWidth - Tolerance.Epsilon;
|
||||
return fitsNormal || fitsRotated;
|
||||
}
|
||||
|
||||
@@ -116,7 +149,8 @@ namespace OpenNest
|
||||
double salvageRate,
|
||||
Plate templatePlate,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token)
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
// Create a temporary plate with candidate size + settings from template.
|
||||
var tempPlate = new Plate(option.Width, option.Length)
|
||||
@@ -132,15 +166,17 @@ namespace OpenNest
|
||||
};
|
||||
|
||||
// Clone items so the dry run doesn't mutate originals.
|
||||
var clonedItems = items.Select(i => new NestItem
|
||||
{
|
||||
Drawing = i.Drawing, // share Drawing reference for BestFitCache compatibility
|
||||
Priority = i.Priority,
|
||||
Quantity = i.Quantity,
|
||||
StepAngle = i.StepAngle,
|
||||
RotationStart = i.RotationStart,
|
||||
RotationEnd = i.RotationEnd,
|
||||
}).ToList();
|
||||
var clonedItems = items
|
||||
.Select(i => new NestItem
|
||||
{
|
||||
Drawing = i.Drawing, // share Drawing reference for BestFitCache compatibility
|
||||
Priority = i.Priority,
|
||||
Quantity = i.Quantity,
|
||||
StepAngle = i.StepAngle,
|
||||
RotationStart = i.RotationStart,
|
||||
RotationEnd = i.RotationEnd,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var engine = NestEngineRegistry.Create(tempPlate);
|
||||
var parts = engine.Nest(clonedItems, progress, token);
|
||||
@@ -158,8 +194,10 @@ namespace OpenNest
|
||||
var costPerSqUnit = option.Cost / option.Area;
|
||||
var netCost = option.Cost - (remnantArea * costPerSqUnit * salvageRate);
|
||||
|
||||
Debug.WriteLine($"[PlateOptimizer] {option.Width}x{option.Length} ${option.Cost}: " +
|
||||
$"{parts.Count} parts, util={partsArea / plateArea:P1}, net=${netCost:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[PlateOptimizer] {option.Width}x{option.Length} ${option.Cost}: "
|
||||
+ $"{parts.Count} parts, util={partsArea / plateArea:P1}, net=${netCost:F2}"
|
||||
);
|
||||
|
||||
return new PlateOptimizerResult
|
||||
{
|
||||
@@ -172,7 +210,8 @@ namespace OpenNest
|
||||
|
||||
private static bool IsBetter(PlateOptimizerResult candidate, PlateOptimizerResult current)
|
||||
{
|
||||
if (current == null) return true;
|
||||
if (current == null)
|
||||
return true;
|
||||
|
||||
// 1. More parts placed is always better.
|
||||
if (candidate.Parts.Count != current.Parts.Count)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.RapidPlanning;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.RapidPlanning;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
@@ -61,7 +61,11 @@ namespace OpenNest.Engine
|
||||
if (i + 1 < sequenced.Count)
|
||||
{
|
||||
var nextStart = ToPartLocal(piercePoints[i + 1], part);
|
||||
cuttingResult = CuttingStrategy.Apply(part.Program, localApproach, nextStart);
|
||||
cuttingResult = CuttingStrategy.Apply(
|
||||
part.Program,
|
||||
localApproach,
|
||||
nextStart
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -82,12 +86,14 @@ namespace OpenNest.Engine
|
||||
|
||||
var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
|
||||
|
||||
results.Add(new ProcessedPart
|
||||
{
|
||||
Part = part,
|
||||
ProcessedProgram = processedProgram,
|
||||
RapidPath = rapidPath
|
||||
});
|
||||
results.Add(
|
||||
new ProcessedPart
|
||||
{
|
||||
Part = part,
|
||||
ProcessedProgram = processedProgram,
|
||||
RapidPath = rapidPath,
|
||||
}
|
||||
);
|
||||
|
||||
var perimeter = GetPartPerimeter(part);
|
||||
if (perimeter != null)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
@@ -13,19 +13,11 @@ namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
if (TravelLineIntersectsShape(travelLine, cutArea))
|
||||
{
|
||||
return new RapidPath
|
||||
{
|
||||
HeadUp = true,
|
||||
Waypoints = new List<Vector>()
|
||||
};
|
||||
return new RapidPath { HeadUp = true, Waypoints = new List<Vector>() };
|
||||
}
|
||||
}
|
||||
|
||||
return new RapidPath
|
||||
{
|
||||
HeadUp = false,
|
||||
Waypoints = new List<Vector>()
|
||||
};
|
||||
return new RapidPath { HeadUp = false, Waypoints = new List<Vector>() };
|
||||
}
|
||||
|
||||
private static bool TravelLineIntersectsShape(Line travelLine, Shape shape)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
@@ -7,11 +7,7 @@ namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
public RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas)
|
||||
{
|
||||
return new RapidPath
|
||||
{
|
||||
HeadUp = true,
|
||||
Waypoints = new List<Vector>()
|
||||
};
|
||||
return new RapidPath { HeadUp = true, Waypoints = new List<Vector>() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
@@ -24,7 +24,7 @@ namespace OpenNest.RectanglePacking
|
||||
{
|
||||
Location = this.Location,
|
||||
Size = this.Size,
|
||||
Items = new List<Item>(Items)
|
||||
Items = new List<Item>(Items),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
@@ -8,11 +8,7 @@ namespace OpenNest.RectanglePacking
|
||||
{
|
||||
public static Bin CreateBin(Box area, double partSpacing)
|
||||
{
|
||||
var bin = new Bin
|
||||
{
|
||||
Location = area.Location,
|
||||
Size = area.Size
|
||||
};
|
||||
var bin = new Bin { Location = area.Location, Size = area.Size };
|
||||
|
||||
bin.Width += partSpacing;
|
||||
bin.Length += partSpacing;
|
||||
@@ -31,7 +27,7 @@ namespace OpenNest.RectanglePacking
|
||||
{
|
||||
Id = id,
|
||||
Location = box.Location,
|
||||
Size = box.Size
|
||||
Size = box.Size,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
internal class FillBestFit : FillEngine
|
||||
{
|
||||
public FillBestFit(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public override void Fill(Item item)
|
||||
{
|
||||
@@ -29,8 +27,7 @@ namespace OpenNest.RectanglePacking
|
||||
Bin.Items.AddRange(bin1.Items);
|
||||
else
|
||||
Bin.Items.AddRange(bin2.Items);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override void Fill(Item item, int maxCount)
|
||||
{
|
||||
@@ -60,8 +57,10 @@ namespace OpenNest.RectanglePacking
|
||||
var normalPrimary = combo.Count1;
|
||||
var rotatePrimary = combo.Count2;
|
||||
|
||||
var normalSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize);
|
||||
var rotateSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / primarySize);
|
||||
var normalSecondary = (int)
|
||||
System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize);
|
||||
var rotateSecondary = (int)
|
||||
System.Math.Floor((binSecondary + Tolerance.Epsilon) / primarySize);
|
||||
|
||||
var (normalRows, normalCols) = horizontal
|
||||
? (normalSecondary, normalPrimary)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
@@ -16,7 +16,13 @@ namespace OpenNest.RectanglePacking
|
||||
|
||||
public abstract void Fill(Item item, int maxCount);
|
||||
|
||||
protected List<Item> FillGrid(Item item, int rows, int columns, int maxCount, bool columnMajor = true)
|
||||
protected List<Item> FillGrid(
|
||||
Item item,
|
||||
int rows,
|
||||
int columns,
|
||||
int maxCount,
|
||||
bool columnMajor = true
|
||||
)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ namespace OpenNest.RectanglePacking
|
||||
internal class FillNoRotation : FillEngine
|
||||
{
|
||||
public FillNoRotation(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public NestDirection NestDirection { get; set; }
|
||||
|
||||
@@ -59,7 +57,9 @@ namespace OpenNest.RectanglePacking
|
||||
columns = (int)System.Math.Ceiling((double)maxCount / rows);
|
||||
}
|
||||
|
||||
Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length));
|
||||
Bin.Items.AddRange(
|
||||
FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ namespace OpenNest.RectanglePacking
|
||||
internal class FillSameRotation : FillEngine
|
||||
{
|
||||
public FillSameRotation(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public override void Fill(Item item)
|
||||
{
|
||||
|
||||
@@ -8,9 +8,7 @@ namespace OpenNest.RectanglePacking
|
||||
public Box CenterRemnant { get; private set; }
|
||||
|
||||
public FillSpiral(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
}
|
||||
: base(bin) { }
|
||||
|
||||
public override void Fill(Item item)
|
||||
{
|
||||
@@ -19,7 +17,8 @@ namespace OpenNest.RectanglePacking
|
||||
|
||||
public override void Fill(Item item, int maxCount)
|
||||
{
|
||||
if (item == null) return;
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
// Width = Y axis, Length = X axis
|
||||
var comboY = BestCombination.FindFrom2(item.Width, item.Length, Bin.Width);
|
||||
@@ -28,15 +27,13 @@ namespace OpenNest.RectanglePacking
|
||||
if (!comboY.Found || !comboX.Found)
|
||||
return;
|
||||
|
||||
var q14size = new Size(
|
||||
item.Width * comboY.Count1,
|
||||
item.Length * comboX.Count1);
|
||||
var q23size = new Size(
|
||||
item.Length * comboY.Count2,
|
||||
item.Width * comboX.Count2);
|
||||
var q14size = new Size(item.Width * comboY.Count1, item.Length * comboX.Count1);
|
||||
var q23size = new Size(item.Length * comboY.Count2, item.Width * comboX.Count2);
|
||||
|
||||
if ((q14size.Width > q23size.Width && q14size.Length > q23size.Length) ||
|
||||
(q23size.Width > q14size.Width && q23size.Length > q14size.Length))
|
||||
if (
|
||||
(q14size.Width > q23size.Width && q14size.Length > q23size.Length)
|
||||
|| (q23size.Width > q14size.Width && q23size.Length > q14size.Length)
|
||||
)
|
||||
return; // cant do an efficient spiral fill
|
||||
|
||||
// Q1: normal orientation at bin origin
|
||||
@@ -57,9 +54,7 @@ namespace OpenNest.RectanglePacking
|
||||
|
||||
// Q4: normal orientation, diagonal from Q1
|
||||
item.Rotate();
|
||||
item.Location = new Vector(
|
||||
Bin.X + q23size.Length,
|
||||
Bin.Y + q23size.Width);
|
||||
item.Location = new Vector(Bin.X + q23size.Length, Bin.Y + q23size.Width);
|
||||
var q4 = FillGrid(item, comboY.Count1, comboX.Count1, maxCount);
|
||||
Bin.Items.AddRange(q4);
|
||||
|
||||
@@ -69,14 +64,21 @@ namespace OpenNest.RectanglePacking
|
||||
var centerW = System.Math.Abs(q14size.Length - q23size.Length);
|
||||
var centerH = System.Math.Abs(q14size.Width - q23size.Width);
|
||||
|
||||
if (comboY.Count1 > 0 && comboY.Count2 > 0 && comboX.Count1 > 0 && comboX.Count2 > 0
|
||||
&& centerW > Tolerance.Epsilon && centerH > Tolerance.Epsilon)
|
||||
if (
|
||||
comboY.Count1 > 0
|
||||
&& comboY.Count2 > 0
|
||||
&& comboX.Count1 > 0
|
||||
&& comboX.Count2 > 0
|
||||
&& centerW > Tolerance.Epsilon
|
||||
&& centerH > Tolerance.Epsilon
|
||||
)
|
||||
{
|
||||
CenterRemnant = new Box(
|
||||
Bin.X + System.Math.Min(q14size.Length, q23size.Length),
|
||||
Bin.Y + System.Math.Min(q14size.Width, q23size.Width),
|
||||
centerW,
|
||||
centerH);
|
||||
centerH
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
@@ -23,7 +23,7 @@ namespace OpenNest.RectanglePacking
|
||||
IsRotated = this.IsRotated,
|
||||
Location = this.Location,
|
||||
Size = this.Size,
|
||||
Id = this.Id
|
||||
Id = this.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,14 @@ namespace OpenNest.RectanglePacking
|
||||
|
||||
foreach (var box in items)
|
||||
{
|
||||
if (box.Left < minX) minX = box.Left;
|
||||
if (box.Right > maxX) maxX = box.Right;
|
||||
if (box.Bottom < minY) minY = box.Bottom;
|
||||
if (box.Top > maxY) maxY = box.Top;
|
||||
if (box.Left < minX)
|
||||
minX = box.Left;
|
||||
if (box.Right > maxX)
|
||||
maxX = box.Right;
|
||||
if (box.Bottom < minY)
|
||||
minY = box.Bottom;
|
||||
if (box.Top > maxY)
|
||||
maxY = box.Top;
|
||||
}
|
||||
|
||||
return new Box(minX, minY, maxX - minX, maxY - minY);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user