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:
aj
2026-09-20 16:41:50 -04:00
parent 8e6fa677fb
commit aec0523062
476 changed files with 16592 additions and 7800 deletions
+71 -35
View File
@@ -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);
+13 -4
View File
@@ -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;
}
+56 -36
View File
@@ -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;
}
+13 -8
View File
@@ -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,
}
}
+286 -157
View File
@@ -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;
+19 -9
View File
@@ -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;
}
}
+5 -3
View File
@@ -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
);
}
}
+16 -8
View File
@@ -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
);
}
}
+42 -14
View File
@@ -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,
};
}
}
+33 -18
View File
@@ -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>();
+10 -9
View File
@@ -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)
{
+18 -10
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit.Tiling
{
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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),
};
}
}
+7 -14
View File
@@ -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++;
+8 -7
View File
@@ -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 -2
View File
@@ -1,5 +1,4 @@

namespace OpenNest.CirclePacking
namespace OpenNest.CirclePacking
{
internal abstract class FillEngine
{
+1 -1
View File
@@ -12,7 +12,7 @@ namespace OpenNest.CirclePacking
{
Radius = this.Radius,
Center = this.Center,
Id = this.Id
Id = this.Id,
};
}
}
+144 -71
View File
@@ -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);
}
}
}
+19 -7
View File
@@ -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;
}
+5 -1
View File
@@ -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;
+102 -37
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
+87 -29
View File
@@ -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();
+69 -33
View File
@@ -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);
+4 -3
View File
@@ -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);
+12 -5
View File
@@ -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;
}
}
+13 -5
View File
@@ -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;
+98 -52
View File
@@ -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;
}
}
+137 -55
View File
@@ -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;
+33 -15
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
+1 -1
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
+29 -12
View File
@@ -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();
+159 -51
View File
@@ -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)
+6 -4
View File
@@ -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);
+50 -30
View File
@@ -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>
+103 -49
View File
@@ -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;
+12 -3
View File
@@ -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 -1
View File
@@ -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);
}
}
+5 -1
View File
@@ -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
);
}
+5 -2
View File
@@ -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
);
}
+9 -3
View File
@@ -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());
+10 -3
View File
@@ -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; }
}
+10 -3
View File
@@ -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; }
+129 -51
View File
@@ -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;
+13 -3
View File
@@ -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
);
+32 -7
View File
@@ -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;
+109 -25
View File
@@ -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
)
);
}
}
}
+42 -12
View File
@@ -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;
}
+11 -3
View File
@@ -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; }
+37 -14
View File
@@ -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}"
);
}
}
}
+48 -12
View File
@@ -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);
+3 -1
View File
@@ -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; }
}
+8 -3
View File
@@ -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}."),
};
}
}
+17 -5
View File
@@ -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);
}
+154 -73
View File
@@ -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;
}
}
+8 -4
View File
@@ -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;
+9 -6
View File
@@ -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);
+25 -9
View File
@@ -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;
}
}
+12 -9
View File
@@ -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;
+118 -68
View File
@@ -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;
+2 -3
View File
@@ -1,9 +1,8 @@

namespace OpenNest
namespace OpenNest
{
public enum NestDirection
{
Vertical,
Horizontal
Horizontal,
}
}
+123 -61
View File
@@ -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;
}
}
}
+43 -17
View File
@@ -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}"
);
}
}
}
+51 -25
View File
@@ -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;
}
+59 -25
View File
@@ -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 };
+39 -16
View File
@@ -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);
+8 -4
View File
@@ -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
);
}
}
+14 -10
View File
@@ -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);
+96 -39
View File
@@ -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,
}
);
}
}
}
+16 -7
View File
@@ -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;
+75 -36
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Engine.RapidPlanning;
using System.Collections.Generic;
namespace OpenNest.Engine
{
+15 -9
View File
@@ -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 -1
View File
@@ -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>() };
}
}
}
+3 -3
View File
@@ -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)
{
+20 -18
View File
@@ -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
);
}
}
}
+11 -7
View File
@@ -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