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
+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;