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:
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -25,7 +25,11 @@ namespace OpenNest.Engine.Fill
|
||||
return new List<double> { 0 };
|
||||
|
||||
case PartType.Rectangle:
|
||||
return new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
return new List<double>
|
||||
{
|
||||
classification.PrimaryAngle,
|
||||
classification.PrimaryAngle + Angle.HalfPI,
|
||||
};
|
||||
|
||||
default:
|
||||
return BuildIrregularAngles(item, classification.PrimaryAngle, workArea);
|
||||
@@ -84,7 +88,11 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static List<double> ApplyMlPrediction(
|
||||
NestItem item, Box workArea, double[] baseAngles, List<double> fallback)
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
double[] baseAngles,
|
||||
List<double> fallback
|
||||
)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features == null)
|
||||
@@ -108,7 +116,9 @@ namespace OpenNest.Engine.Fill
|
||||
mlAngles.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total");
|
||||
Debug.WriteLine(
|
||||
$"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total"
|
||||
);
|
||||
return mlAngles;
|
||||
}
|
||||
|
||||
@@ -121,7 +131,9 @@ namespace OpenNest.Engine.Fill
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
|
||||
Debug.WriteLine(
|
||||
$"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)"
|
||||
);
|
||||
return pruned;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ namespace OpenNest
|
||||
|
||||
internal static class BestCombination
|
||||
{
|
||||
public static CombinationResult FindFrom2(double length1, double length2, double overallLength)
|
||||
public static CombinationResult FindFrom2(
|
||||
double length1,
|
||||
double length2,
|
||||
double overallLength
|
||||
)
|
||||
{
|
||||
overallLength += Tolerance.Epsilon;
|
||||
var count1 = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
@@ -14,8 +14,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
@@ -26,8 +26,8 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
@@ -37,8 +37,13 @@ namespace OpenNest.Engine.Fill
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, Vector direction)
|
||||
public static double Push(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
Vector direction
|
||||
)
|
||||
{
|
||||
var opposite = -direction;
|
||||
|
||||
@@ -84,33 +89,59 @@ namespace OpenNest.Engine.Fill
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var obstacleSpacingBox = obstacleSpacingBoxes[i];
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, opposite);
|
||||
var reverseGap = SpatialQuery.DirectionalGap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
opposite
|
||||
);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, direction);
|
||||
var gap = SpatialQuery.DirectionalGap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
direction
|
||||
);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingSpacingBox, obstacleSpacingBox, direction))
|
||||
if (
|
||||
!SpatialQuery.PerpendicularOverlap(
|
||||
movingSpacingBox,
|
||||
obstacleSpacingBox,
|
||||
direction
|
||||
)
|
||||
)
|
||||
continue;
|
||||
|
||||
movingEntities ??= halfSpacing > 0
|
||||
? (needCutouts
|
||||
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
|
||||
: (needCutouts
|
||||
? PartGeometry.GetPartEntities(moving)
|
||||
: PartGeometry.GetPerimeterEntities(moving));
|
||||
movingEntities ??=
|
||||
halfSpacing > 0
|
||||
? (
|
||||
needCutouts
|
||||
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing)
|
||||
)
|
||||
: (
|
||||
needCutouts
|
||||
? PartGeometry.GetPartEntities(moving)
|
||||
: PartGeometry.GetPerimeterEntities(moving)
|
||||
);
|
||||
|
||||
obstacleEntities[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||
obstacleEntities[i] ??=
|
||||
halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
||||
if (d <= Tolerance.Epsilon
|
||||
var d = SpatialQuery.DirectionalDistance(
|
||||
movingEntities,
|
||||
obstacleEntities[i],
|
||||
direction
|
||||
);
|
||||
if (
|
||||
d <= Tolerance.Epsilon
|
||||
&& partSpacing <= Tolerance.Epsilon
|
||||
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction))
|
||||
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -133,8 +164,12 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static Box SpacingBounds(Box box, double spacing)
|
||||
{
|
||||
return new Box(box.Left - spacing, box.Bottom - spacing,
|
||||
box.Length + 2 * spacing, box.Width + 2 * spacing);
|
||||
return new Box(
|
||||
box.Left - spacing,
|
||||
box.Bottom - spacing,
|
||||
box.Length + 2 * spacing,
|
||||
box.Width + 2 * spacing
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
||||
@@ -162,8 +197,13 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
public static double Push(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
|
||||
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
|
||||
@@ -174,17 +214,32 @@ namespace OpenNest.Engine.Fill
|
||||
/// Much faster but less precise — use as a coarse positioning pass before
|
||||
/// a full geometry Push.
|
||||
/// </summary>
|
||||
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
public static double PushBoundingBox(
|
||||
List<Part> movingParts,
|
||||
Plate plate,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
var obstacleParts = plate
|
||||
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||
.ToList();
|
||||
|
||||
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
return PushBoundingBox(
|
||||
movingParts,
|
||||
obstacleParts,
|
||||
plate.WorkArea(),
|
||||
plate.PartSpacing,
|
||||
direction
|
||||
);
|
||||
}
|
||||
|
||||
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
public static double PushBoundingBox(
|
||||
List<Part> movingParts,
|
||||
List<Part> obstacleParts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
@@ -206,7 +261,11 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
var reverseGap = SpatialQuery.DirectionalGap(
|
||||
movingBox,
|
||||
obstacleBoxes[i],
|
||||
opposite
|
||||
);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
@@ -219,7 +278,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing - 0.002;
|
||||
if (d < 0) d = 0;
|
||||
if (d < 0)
|
||||
d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
@@ -240,8 +300,13 @@ namespace OpenNest.Engine.Fill
|
||||
/// Repeatedly pushes parts left then down until total movement per
|
||||
/// iteration falls below the given threshold.
|
||||
/// </summary>
|
||||
public static void Settle(List<Part> parts, Box workArea, double partSpacing,
|
||||
double threshold = 0.01, int maxIterations = 20)
|
||||
public static void Settle(
|
||||
List<Part> parts,
|
||||
Box workArea,
|
||||
double partSpacing,
|
||||
double threshold = 0.01,
|
||||
int maxIterations = 20
|
||||
)
|
||||
{
|
||||
if (parts.Count < 2)
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -23,9 +23,12 @@ namespace OpenNest.Engine.Fill
|
||||
halfSpacing = partSpacing / 2;
|
||||
}
|
||||
|
||||
public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
|
||||
public List<Part> Fill(
|
||||
Drawing drawing,
|
||||
double rotationAngle = 0,
|
||||
CancellationToken token = default,
|
||||
Action<List<Part>, string> reportProgress = null)
|
||||
Action<List<Part>, string> reportProgress = null
|
||||
)
|
||||
{
|
||||
var pair = BuildPair(drawing, rotationAngle);
|
||||
if (pair == null)
|
||||
@@ -64,8 +67,10 @@ namespace OpenNest.Engine.Fill
|
||||
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
|
||||
|
||||
// Check that each part fits in the work area individually.
|
||||
if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return null;
|
||||
|
||||
// Slide part2 toward part1 from the right using geometry-aware distance.
|
||||
@@ -80,7 +85,11 @@ namespace OpenNest.Engine.Fill
|
||||
// Slide part2 left toward part1.
|
||||
var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left);
|
||||
var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right);
|
||||
var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
|
||||
var dist = SpatialQuery.DirectionalDistance(
|
||||
movingLines,
|
||||
stationaryLines,
|
||||
PushDirection.Left
|
||||
);
|
||||
|
||||
if (dist < double.MaxValue && dist > 0)
|
||||
{
|
||||
@@ -93,8 +102,10 @@ namespace OpenNest.Engine.Fill
|
||||
return null;
|
||||
|
||||
// Verify pair fits in work area.
|
||||
if (pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon
|
||||
|| pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return null;
|
||||
|
||||
return pair;
|
||||
@@ -121,8 +132,14 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
// Find minimum distance from test pair sliding down toward original pair.
|
||||
var copyDistance = FindVerticalCopyDistance(
|
||||
pair.Part1, pair.Part2, testPart1, testPart2,
|
||||
boundary1, boundary2, pairHeight);
|
||||
pair.Part1,
|
||||
pair.Part2,
|
||||
testPart1,
|
||||
testPart2,
|
||||
boundary1,
|
||||
boundary2,
|
||||
pairHeight
|
||||
);
|
||||
|
||||
if (copyDistance <= 0)
|
||||
return column;
|
||||
@@ -144,25 +161,56 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private double FindVerticalCopyDistance(
|
||||
Part origPart1, Part origPart2,
|
||||
Part testPart1, Part testPart2,
|
||||
PartBoundary boundary1, PartBoundary boundary2,
|
||||
double pairHeight)
|
||||
Part origPart1,
|
||||
Part origPart2,
|
||||
Part testPart1,
|
||||
Part testPart2,
|
||||
PartBoundary boundary1,
|
||||
PartBoundary boundary2,
|
||||
double pairHeight
|
||||
)
|
||||
{
|
||||
// Check all 4 combinations: test parts sliding down toward original parts.
|
||||
var slidePairs = new[]
|
||||
{
|
||||
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
|
||||
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
|
||||
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
|
||||
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
|
||||
(
|
||||
moving: boundary1,
|
||||
movingLoc: testPart1.Location,
|
||||
stationary: boundary1,
|
||||
stationaryLoc: origPart1.Location
|
||||
),
|
||||
(
|
||||
moving: boundary1,
|
||||
movingLoc: testPart1.Location,
|
||||
stationary: boundary2,
|
||||
stationaryLoc: origPart2.Location
|
||||
),
|
||||
(
|
||||
moving: boundary2,
|
||||
movingLoc: testPart2.Location,
|
||||
stationary: boundary1,
|
||||
stationaryLoc: origPart1.Location
|
||||
),
|
||||
(
|
||||
moving: boundary2,
|
||||
movingLoc: testPart2.Location,
|
||||
stationary: boundary2,
|
||||
stationaryLoc: origPart2.Location
|
||||
),
|
||||
};
|
||||
|
||||
var minSlide = double.MaxValue;
|
||||
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
|
||||
{
|
||||
var d = SlideDistance(moving, movingLoc, stationary, stationaryLoc, PushDirection.Down);
|
||||
if (d < minSlide) minSlide = d;
|
||||
var d = SlideDistance(
|
||||
moving,
|
||||
movingLoc,
|
||||
stationary,
|
||||
stationaryLoc,
|
||||
PushDirection.Down
|
||||
);
|
||||
if (d < minSlide)
|
||||
minSlide = d;
|
||||
}
|
||||
|
||||
if (minSlide >= double.MaxValue || minSlide < 0)
|
||||
@@ -177,18 +225,24 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static double SlideDistance(
|
||||
PartBoundary movingBoundary, Vector movingLocation,
|
||||
PartBoundary stationaryBoundary, Vector stationaryLocation,
|
||||
PushDirection direction)
|
||||
PartBoundary movingBoundary,
|
||||
Vector movingLocation,
|
||||
PartBoundary stationaryBoundary,
|
||||
Vector stationaryLocation,
|
||||
PushDirection direction
|
||||
)
|
||||
{
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var movingEdges = movingBoundary.GetEdges(direction);
|
||||
var stationaryEdges = stationaryBoundary.GetEdges(opposite);
|
||||
|
||||
return SpatialQuery.DirectionalDistance(
|
||||
movingEdges, movingLocation,
|
||||
stationaryEdges, stationaryLocation,
|
||||
direction);
|
||||
movingEdges,
|
||||
movingLocation,
|
||||
stationaryEdges,
|
||||
stationaryLocation,
|
||||
direction
|
||||
);
|
||||
}
|
||||
|
||||
// --- Step 3: Iterative Adjustment ---
|
||||
@@ -249,7 +303,11 @@ namespace OpenNest.Engine.Fill
|
||||
return TryShiftDirection(pair, -adjustment, originalPairWidth);
|
||||
}
|
||||
|
||||
private PartPair? TryShiftDirection(PartPair pair, double verticalShift, double originalPairWidth)
|
||||
private PartPair? TryShiftDirection(
|
||||
PartPair pair,
|
||||
double verticalShift,
|
||||
double originalPairWidth
|
||||
)
|
||||
{
|
||||
// Clone parts so we don't mutate the originals.
|
||||
var p1 = (Part)pair.Part1.Clone();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -34,9 +34,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static PushDirection GetPushDirection(NestDirection direction)
|
||||
{
|
||||
return direction == NestDirection.Horizontal
|
||||
? PushDirection.Left
|
||||
: PushDirection.Down;
|
||||
return direction == NestDirection.Horizontal ? PushDirection.Left : PushDirection.Down;
|
||||
}
|
||||
|
||||
private static double GetDimension(Box box, NestDirection direction)
|
||||
@@ -75,10 +73,15 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
|
||||
var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
|
||||
partA.CloneAtOffset(offset), HalfSpacing);
|
||||
partA.CloneAtOffset(offset),
|
||||
HalfSpacing
|
||||
);
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEntities, stationaryEntities, pushDir);
|
||||
movingEntities,
|
||||
stationaryEntities,
|
||||
pushDir
|
||||
);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
return bboxDim + PartSpacing;
|
||||
@@ -140,12 +143,19 @@ namespace OpenNest.Engine.Fill
|
||||
continue;
|
||||
|
||||
stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[i], HalfSpacing);
|
||||
parts[i],
|
||||
HalfSpacing
|
||||
);
|
||||
movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||
parts[j].CloneAtOffset(offset), HalfSpacing);
|
||||
parts[j].CloneAtOffset(offset),
|
||||
HalfSpacing
|
||||
);
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEntities[j], stationaryEntities[i], pushDir);
|
||||
movingEntities[j],
|
||||
stationaryEntities[i],
|
||||
pushDir
|
||||
);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
continue;
|
||||
@@ -209,10 +219,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var part = basePart.CloneAtOffset(offset);
|
||||
|
||||
if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon &&
|
||||
part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon &&
|
||||
part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon &&
|
||||
part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon)
|
||||
if (
|
||||
part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon
|
||||
&& part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon
|
||||
&& part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon
|
||||
&& part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon
|
||||
)
|
||||
{
|
||||
result.Add(part);
|
||||
}
|
||||
@@ -258,7 +270,11 @@ namespace OpenNest.Engine.Fill
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool HasOverlappingParts(List<Part> parts, out int overlapA, out int overlapB)
|
||||
private static bool HasOverlappingParts(
|
||||
List<Part> parts,
|
||||
out int overlapA,
|
||||
out int overlapB
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
@@ -268,10 +284,10 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var b2 = parts[j].BoundingBox;
|
||||
|
||||
var overlapX = System.Math.Min(b1.Right, b2.Right)
|
||||
- System.Math.Max(b1.Left, b2.Left);
|
||||
var overlapY = System.Math.Min(b1.Top, b2.Top)
|
||||
- System.Math.Max(b1.Bottom, b2.Bottom);
|
||||
var overlapX =
|
||||
System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
|
||||
var overlapY =
|
||||
System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
|
||||
|
||||
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
|
||||
continue;
|
||||
@@ -305,8 +321,10 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
template.Offset(WorkArea.Location - template.BoundingBox.Location);
|
||||
|
||||
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||
template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|
||||
|| template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return pattern;
|
||||
|
||||
pattern.Parts.Add(template);
|
||||
@@ -367,8 +385,14 @@ namespace OpenNest.Engine.Fill
|
||||
return gridResult;
|
||||
}
|
||||
|
||||
private void LogOverlap(string step, NestDirection tilingDir,
|
||||
Pattern pattern, List<Part> parts, int idxA, int idxB)
|
||||
private void LogOverlap(
|
||||
string step,
|
||||
NestDirection tilingDir,
|
||||
Pattern pattern,
|
||||
List<Part> parts,
|
||||
int idxA,
|
||||
int idxB
|
||||
)
|
||||
{
|
||||
var pa = parts[idxA];
|
||||
var pb = parts[idxB];
|
||||
@@ -377,22 +401,32 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})");
|
||||
Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}");
|
||||
Debug.WriteLine($" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}");
|
||||
Debug.WriteLine($" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}");
|
||||
Debug.WriteLine(
|
||||
$" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}"
|
||||
);
|
||||
Debug.WriteLine($" Total parts after tiling: {parts.Count}");
|
||||
Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:");
|
||||
Debug.WriteLine($" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " +
|
||||
$"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})");
|
||||
Debug.WriteLine($" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " +
|
||||
$"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})");
|
||||
Debug.WriteLine(
|
||||
$" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° "
|
||||
+ $"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° "
|
||||
+ $"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})"
|
||||
);
|
||||
|
||||
// Log all pattern seed parts for reproduction
|
||||
Debug.WriteLine($" Pattern seed parts:");
|
||||
for (var i = 0; i < pattern.Parts.Count; i++)
|
||||
{
|
||||
var p = pattern.Parts[i];
|
||||
Debug.WriteLine($" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " +
|
||||
$"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}");
|
||||
Debug.WriteLine(
|
||||
$" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° "
|
||||
+ $"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,8 +480,10 @@ namespace OpenNest.Engine.Fill
|
||||
var offset = WorkArea.Location - pattern.BoundingBox.Location;
|
||||
var basePattern = pattern.Clone(offset);
|
||||
|
||||
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||
if (
|
||||
basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|
||||
|| basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
|
||||
)
|
||||
return new List<Part>();
|
||||
|
||||
return FillGrid(basePattern, primaryAxis);
|
||||
|
||||
@@ -76,9 +76,10 @@ public static class FillResultCache
|
||||
}
|
||||
|
||||
public bool Equals(CacheKey other) =>
|
||||
ReferenceEquals(Drawing, other.Drawing) &&
|
||||
Width == other.Width && Height == other.Height &&
|
||||
Spacing == other.Spacing;
|
||||
ReferenceEquals(Drawing, other.Drawing)
|
||||
&& Width == other.Width
|
||||
&& Height == other.Height
|
||||
&& Spacing == other.Spacing;
|
||||
|
||||
public override bool Equals(object obj) => obj is CacheKey other && Equals(other);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -37,10 +37,14 @@ namespace OpenNest.Engine.Fill
|
||||
totalPartArea += part.BaseDrawing.Area;
|
||||
var bb = part.BoundingBox;
|
||||
|
||||
if (bb.Left < minX) minX = bb.Left;
|
||||
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||
if (bb.Right > maxX) maxX = bb.Right;
|
||||
if (bb.Top > maxY) maxY = bb.Top;
|
||||
if (bb.Left < minX)
|
||||
minX = bb.Left;
|
||||
if (bb.Bottom < minY)
|
||||
minY = bb.Bottom;
|
||||
if (bb.Right > maxX)
|
||||
maxX = bb.Right;
|
||||
if (bb.Top > maxY)
|
||||
maxY = bb.Top;
|
||||
}
|
||||
|
||||
var bboxArea = (maxX - minX) * (maxY - minY);
|
||||
@@ -63,8 +67,11 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
public static bool operator >(FillScore a, FillScore b) => a.CompareTo(b) > 0;
|
||||
|
||||
public static bool operator <(FillScore a, FillScore b) => a.CompareTo(b) < 0;
|
||||
|
||||
public static bool operator >=(FillScore a, FillScore b) => a.CompareTo(b) >= 0;
|
||||
|
||||
public static bool operator <=(FillScore a, FillScore b) => a.CompareTo(b) <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ public class GridDedup
|
||||
/// <summary>
|
||||
/// Gets or creates a GridDedup from FillContext.SharedState.
|
||||
/// </summary>
|
||||
public static GridDedup GetOrCreate(System.Collections.Generic.Dictionary<string, object> sharedState)
|
||||
public static GridDedup GetOrCreate(
|
||||
System.Collections.Generic.Dictionary<string, object> sharedState
|
||||
)
|
||||
{
|
||||
if (sharedState.TryGetValue(SharedStateKey, out var existing))
|
||||
return (GridDedup)existing;
|
||||
@@ -41,7 +43,11 @@ public class GridDedup
|
||||
|
||||
private readonly struct GridKey : IEquatable<GridKey>
|
||||
{
|
||||
private readonly int _patternW, _patternL, _workW, _workL, _dir;
|
||||
private readonly int _patternW,
|
||||
_patternL,
|
||||
_workW,
|
||||
_workL,
|
||||
_dir;
|
||||
|
||||
public GridKey(Box patternBox, Box workArea, NestDirection dir)
|
||||
{
|
||||
@@ -53,9 +59,11 @@ public class GridDedup
|
||||
}
|
||||
|
||||
public bool Equals(GridKey other) =>
|
||||
_patternW == other._patternW && _patternL == other._patternL &&
|
||||
_workW == other._workW && _workL == other._workL &&
|
||||
_dir == other._dir;
|
||||
_patternW == other._patternW
|
||||
&& _patternL == other._patternL
|
||||
&& _workW == other._workW
|
||||
&& _workL == other._workL
|
||||
&& _dir == other._dir;
|
||||
|
||||
public override bool Equals(object obj) => obj is GridKey other && Equals(other);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill
|
||||
return candExtent < currExtent;
|
||||
|
||||
return FillScore.Compute(candidate, workArea).Density
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
}
|
||||
|
||||
private static double YExtent(List<Part> parts)
|
||||
@@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var bb = part.BoundingBox;
|
||||
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||
if (bb.Top > maxY) maxY = bb.Top;
|
||||
if (bb.Bottom < minY)
|
||||
minY = bb.Bottom;
|
||||
if (bb.Top > maxY)
|
||||
maxY = bb.Top;
|
||||
}
|
||||
|
||||
return maxY - minY;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -33,7 +33,8 @@ namespace OpenNest.Engine.Fill
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null,
|
||||
int plateNumber = 0,
|
||||
Func<NestItem, Box, List<Part>> widthFillFunc = null)
|
||||
Func<NestItem, Box, List<Part>> widthFillFunc = null
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new IterativeShrinkResult();
|
||||
@@ -48,19 +49,20 @@ namespace OpenNest.Engine.Fill
|
||||
if (item.Quantity <= 0)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
var estimatedMax = bbox.Area() > 0
|
||||
? (int)(workArea.Area() / bbox.Area()) * 2
|
||||
: 1000;
|
||||
var estimatedMax =
|
||||
bbox.Area() > 0 ? (int)(workArea.Area() / bbox.Area()) * 2 : 1000;
|
||||
|
||||
workItems.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = System.Math.Max(1, estimatedMax),
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
workItems.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = System.Math.Max(1, estimatedMax),
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -86,10 +88,32 @@ namespace OpenNest.Engine.Fill
|
||||
ShrinkResult widthResult = null;
|
||||
|
||||
Parallel.Invoke(
|
||||
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
|
||||
() => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
|
||||
() =>
|
||||
heightResult = ShrinkFiller.Shrink(
|
||||
fillFunc,
|
||||
ni,
|
||||
box,
|
||||
spacing,
|
||||
ShrinkAxis.Length,
|
||||
token,
|
||||
targetCount: target,
|
||||
progress: progress,
|
||||
plateNumber: plateNumber,
|
||||
placedParts: placedSoFar
|
||||
),
|
||||
() =>
|
||||
widthResult = ShrinkFiller.Shrink(
|
||||
wFillFunc,
|
||||
ni,
|
||||
box,
|
||||
spacing,
|
||||
ShrinkAxis.Width,
|
||||
token,
|
||||
targetCount: target,
|
||||
progress: progress,
|
||||
plateNumber: plateNumber,
|
||||
placedParts: placedSoFar
|
||||
)
|
||||
);
|
||||
|
||||
var heightScore = FillScore.Compute(heightResult.Parts, box);
|
||||
@@ -112,15 +136,18 @@ namespace OpenNest.Engine.Fill
|
||||
var allParts = new List<Part>(placedSoFar.Count + best.Count);
|
||||
allParts.AddRange(placedSoFar);
|
||||
allParts.AddRange(best);
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = box,
|
||||
Description = $"Shrink: {best.Count} parts placed",
|
||||
IsOverallBest = true,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = box,
|
||||
Description = $"Shrink: {best.Count} parts placed",
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Accumulate for the next item's progress reports.
|
||||
@@ -136,8 +163,7 @@ namespace OpenNest.Engine.Fill
|
||||
var leftovers = new List<NestItem>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var placedCount = placed.Count(p =>
|
||||
ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
var placedCount = placed.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing));
|
||||
|
||||
if (item.Quantity <= 0)
|
||||
continue; // unlimited items are always "satisfied" — no leftover
|
||||
@@ -145,15 +171,17 @@ namespace OpenNest.Engine.Fill
|
||||
var remaining = item.Quantity - placedCount;
|
||||
if (remaining > 0)
|
||||
{
|
||||
leftovers.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = remaining,
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
leftovers.Add(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = remaining,
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,29 +193,43 @@ namespace OpenNest.Engine.Fill
|
||||
/// a staircase profile that maximizes usable remnant area.
|
||||
/// </summary>
|
||||
internal static void SortColumnsByHeight(List<Part> parts, double spacing) =>
|
||||
SortStrips(parts, spacing,
|
||||
primaryEdge: b => b.Left, extentEdge: b => b.Right,
|
||||
sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight,
|
||||
makeOffset: d => new Vector(d, 0));
|
||||
SortStrips(
|
||||
parts,
|
||||
spacing,
|
||||
primaryEdge: b => b.Left,
|
||||
extentEdge: b => b.Right,
|
||||
sortMetric: MaxTop,
|
||||
stripMin: MinLeft,
|
||||
stripMax: MaxRight,
|
||||
makeOffset: d => new Vector(d, 0)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts pair rows by width (narrowest first on the bottom) to create
|
||||
/// a staircase profile on the right side that maximizes usable remnant area.
|
||||
/// </summary>
|
||||
internal static void SortRowsByWidth(List<Part> parts, double spacing) =>
|
||||
SortStrips(parts, spacing,
|
||||
primaryEdge: b => b.Bottom, extentEdge: b => b.Top,
|
||||
sortMetric: MaxRight, stripMin: MinBottom, stripMax: MaxTop,
|
||||
makeOffset: d => new Vector(0, d));
|
||||
SortStrips(
|
||||
parts,
|
||||
spacing,
|
||||
primaryEdge: b => b.Bottom,
|
||||
extentEdge: b => b.Top,
|
||||
sortMetric: MaxRight,
|
||||
stripMin: MinBottom,
|
||||
stripMax: MaxTop,
|
||||
makeOffset: d => new Vector(0, d)
|
||||
);
|
||||
|
||||
private static void SortStrips(
|
||||
List<Part> parts, double spacing,
|
||||
List<Part> parts,
|
||||
double spacing,
|
||||
Func<Box, double> primaryEdge,
|
||||
Func<Box, double> extentEdge,
|
||||
Func<List<Part>, double> sortMetric,
|
||||
Func<List<Part>, double> stripMin,
|
||||
Func<List<Part>, double> stripMax,
|
||||
Func<double, Vector> makeOffset)
|
||||
Func<double, Vector> makeOffset
|
||||
)
|
||||
{
|
||||
if (parts == null || parts.Count <= 1)
|
||||
return;
|
||||
@@ -250,7 +292,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var max = double.MinValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Top > max) max = p.BoundingBox.Top;
|
||||
if (p.BoundingBox.Top > max)
|
||||
max = p.BoundingBox.Top;
|
||||
return max;
|
||||
}
|
||||
|
||||
@@ -258,7 +301,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var max = double.MinValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Right > max) max = p.BoundingBox.Right;
|
||||
if (p.BoundingBox.Right > max)
|
||||
max = p.BoundingBox.Right;
|
||||
return max;
|
||||
}
|
||||
|
||||
@@ -266,7 +310,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var min = double.MaxValue;
|
||||
foreach (var p in col)
|
||||
if (p.BoundingBox.Left < min) min = p.BoundingBox.Left;
|
||||
if (p.BoundingBox.Left < min)
|
||||
min = p.BoundingBox.Left;
|
||||
return min;
|
||||
}
|
||||
|
||||
@@ -274,7 +319,8 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var min = double.MaxValue;
|
||||
foreach (var p in row)
|
||||
if (p.BoundingBox.Bottom < min) min = p.BoundingBox.Bottom;
|
||||
if (p.BoundingBox.Bottom < min)
|
||||
min = p.BoundingBox.Bottom;
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -9,6 +5,10 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -44,28 +44,49 @@ namespace OpenNest.Engine.Fill
|
||||
this.dedup = dedup ?? new GridDedup();
|
||||
}
|
||||
|
||||
public PairFillResult Fill(NestItem item, Box workArea,
|
||||
public PairFillResult Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
CancellationToken token = default,
|
||||
Action<List<Part>, string> reportProgress = null)
|
||||
Action<List<Part>, string> reportProgress = null
|
||||
)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, plateSize.Length, plateSize.Width, partSpacing);
|
||||
item.Drawing,
|
||||
plateSize.Length,
|
||||
plateSize.Width,
|
||||
partSpacing
|
||||
);
|
||||
|
||||
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
|
||||
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"
|
||||
);
|
||||
|
||||
var targetCount = item.Quantity > 0 ? item.Quantity : 0;
|
||||
var parts = EvaluateCandidates(candidates, item.Drawing, workArea, targetCount,
|
||||
token, reportProgress);
|
||||
var parts = EvaluateCandidates(
|
||||
candidates,
|
||||
item.Drawing,
|
||||
workArea,
|
||||
targetCount,
|
||||
token,
|
||||
reportProgress
|
||||
);
|
||||
|
||||
return new PairFillResult { Parts = parts, BestFits = bestFits };
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidates(
|
||||
List<BestFitResult> candidates, Drawing drawing,
|
||||
Box workArea, int targetCount,
|
||||
CancellationToken token, Action<List<Part>, string> reportProgress)
|
||||
List<BestFitResult> candidates,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
int targetCount,
|
||||
CancellationToken token,
|
||||
Action<List<Part>, string> reportProgress
|
||||
)
|
||||
{
|
||||
List<Part> best = null;
|
||||
var sinceImproved = 0;
|
||||
@@ -89,14 +110,23 @@ namespace OpenNest.Engine.Fill
|
||||
var minCountToBeat = best?.Count ?? 0;
|
||||
|
||||
var results = new List<Part>[batchCount];
|
||||
Parallel.For(0, batchCount,
|
||||
Parallel.For(
|
||||
0,
|
||||
batchCount,
|
||||
new ParallelOptions { CancellationToken = token },
|
||||
j =>
|
||||
{
|
||||
results[j] = EvaluateCandidate(
|
||||
candidates[batchStart + j], drawing, batchWorkArea,
|
||||
minCountToBeat, maxUtilization, partArea, token);
|
||||
});
|
||||
candidates[batchStart + j],
|
||||
drawing,
|
||||
batchWorkArea,
|
||||
minCountToBeat,
|
||||
maxUtilization,
|
||||
partArea,
|
||||
token
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
for (var j = 0; j < batchCount; j++)
|
||||
{
|
||||
@@ -104,20 +134,29 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
best = results[j];
|
||||
sinceImproved = 0;
|
||||
effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea);
|
||||
effectiveWorkArea = TryReduceWorkArea(
|
||||
best,
|
||||
targetCount,
|
||||
workArea,
|
||||
effectiveWorkArea
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
sinceImproved++;
|
||||
}
|
||||
|
||||
reportProgress?.Invoke(best,
|
||||
$"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
|
||||
reportProgress?.Invoke(
|
||||
best,
|
||||
$"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts"
|
||||
);
|
||||
}
|
||||
|
||||
if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +174,12 @@ namespace OpenNest.Engine.Fill
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
private static Box TryReduceWorkArea(List<Part> parts, int targetCount, Box workArea, Box effectiveWorkArea)
|
||||
private static Box TryReduceWorkArea(
|
||||
List<Part> parts,
|
||||
int targetCount,
|
||||
Box workArea,
|
||||
Box effectiveWorkArea
|
||||
)
|
||||
{
|
||||
if (targetCount <= 0 || parts.Count <= targetCount)
|
||||
return effectiveWorkArea;
|
||||
@@ -144,7 +188,9 @@ namespace OpenNest.Engine.Fill
|
||||
if (reduced.Area() >= effectiveWorkArea.Area())
|
||||
return effectiveWorkArea;
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)"
|
||||
);
|
||||
return reduced;
|
||||
}
|
||||
|
||||
@@ -158,23 +204,30 @@ namespace OpenNest.Engine.Fill
|
||||
if (parts.Count <= targetCount)
|
||||
return workArea;
|
||||
|
||||
var sorted = parts
|
||||
.OrderByDescending(p => p.BoundingBox.Top)
|
||||
.ToList();
|
||||
var sorted = parts.OrderByDescending(p => p.BoundingBox.Top).ToList();
|
||||
|
||||
var trimCount = sorted.Count - targetCount;
|
||||
var remaining = sorted.Skip(trimCount).ToList();
|
||||
|
||||
var newTop = remaining.Max(p => p.BoundingBox.Top);
|
||||
|
||||
return new Box(workArea.X, workArea.Y,
|
||||
return new Box(
|
||||
workArea.X,
|
||||
workArea.Y,
|
||||
workArea.Length,
|
||||
System.Math.Min(newTop - workArea.Y, workArea.Width));
|
||||
System.Math.Min(newTop - workArea.Y, workArea.Width)
|
||||
);
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
|
||||
Box workArea, int minCountToBeat, double maxUtilization, double partArea,
|
||||
CancellationToken token)
|
||||
private List<Part> EvaluateCandidate(
|
||||
BestFitResult candidate,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
int minCountToBeat,
|
||||
double maxUtilization,
|
||||
double partArea,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
var angles = BuildTilingAngles(candidate);
|
||||
@@ -211,10 +264,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var topCount = grids[0].Parts.Count;
|
||||
var optimisticRemnant = EstimateRemnantUpperBound(
|
||||
grids[0].Parts, workArea, maxUtilization, partArea);
|
||||
grids[0].Parts,
|
||||
workArea,
|
||||
maxUtilization,
|
||||
partArea
|
||||
);
|
||||
if (topCount + optimisticRemnant <= minCountToBeat)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +289,11 @@ namespace OpenNest.Engine.Fill
|
||||
if (best != null)
|
||||
{
|
||||
var remnantBound = EstimateRemnantUpperBound(
|
||||
gridParts, workArea, maxUtilization, partArea);
|
||||
gridParts,
|
||||
workArea,
|
||||
maxUtilization,
|
||||
partArea
|
||||
);
|
||||
if (gridParts.Count + remnantBound <= best.Count)
|
||||
break; // sorted descending, so remaining are even smaller
|
||||
}
|
||||
@@ -255,8 +318,12 @@ namespace OpenNest.Engine.Fill
|
||||
return best;
|
||||
}
|
||||
|
||||
private int EstimateRemnantUpperBound(List<Part> gridParts, Box workArea,
|
||||
double maxUtilization, double partArea)
|
||||
private int EstimateRemnantUpperBound(
|
||||
List<Part> gridParts,
|
||||
Box workArea,
|
||||
double maxUtilization,
|
||||
double partArea
|
||||
)
|
||||
{
|
||||
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||
|
||||
@@ -271,8 +338,12 @@ namespace OpenNest.Engine.Fill
|
||||
return (int)(remnantArea * maxUtilization / partArea) + 1;
|
||||
}
|
||||
|
||||
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
|
||||
Box workArea, CancellationToken token)
|
||||
private List<Part> FillRemnant(
|
||||
List<Part> gridParts,
|
||||
Drawing drawing,
|
||||
Box workArea,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||
var partBox = drawing.Program.BoundingBox();
|
||||
@@ -322,14 +393,19 @@ namespace OpenNest.Engine.Fill
|
||||
token.ThrowIfCancellationRequested();
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => filler.Fill(drawing, angle, dir),
|
||||
null, comparer, remnantBox);
|
||||
null,
|
||||
comparer,
|
||||
remnantBox
|
||||
);
|
||||
|
||||
if (result != null && result.Count > (parts?.Count ?? 0))
|
||||
parts = result;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
|
||||
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Remnant: {parts?.Count ?? 0} parts in "
|
||||
+ $"{remnantBox.Width:F2}x{remnantBox.Length:F2}"
|
||||
);
|
||||
|
||||
if (parts != null && parts.Count > 0)
|
||||
{
|
||||
@@ -365,16 +441,19 @@ namespace OpenNest.Engine.Fill
|
||||
if (workShortSide < plateShortSide * 0.5)
|
||||
{
|
||||
// Strip mode: prioritize candidates that fit the narrow dimension.
|
||||
var stripCandidates = kept
|
||||
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= MinStripUtilization)
|
||||
var stripCandidates = kept.Where(r =>
|
||||
r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= MinStripUtilization
|
||||
)
|
||||
.ToList();
|
||||
|
||||
SortByEstimatedCount(stripCandidates, workArea);
|
||||
|
||||
var top = stripCandidates.Take(MaxStripCandidates).ToList();
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
||||
Debug.WriteLine(
|
||||
$"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"
|
||||
);
|
||||
return top;
|
||||
}
|
||||
|
||||
@@ -389,16 +468,18 @@ namespace OpenNest.Engine.Fill
|
||||
var w = workArea.Width;
|
||||
var l = workArea.Length;
|
||||
|
||||
candidates.Sort((a, b) =>
|
||||
{
|
||||
var aCount = EstimateTileCount(a, w, l);
|
||||
var bCount = EstimateTileCount(b, w, l);
|
||||
candidates.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var aCount = EstimateTileCount(a, w, l);
|
||||
var bCount = EstimateTileCount(b, w, l);
|
||||
|
||||
if (aCount != bCount)
|
||||
return bCount.CompareTo(aCount);
|
||||
if (aCount != bCount)
|
||||
return bCount.CompareTo(aCount);
|
||||
|
||||
return b.Utilization.CompareTo(a.Utilization);
|
||||
});
|
||||
return b.Utilization.CompareTo(a.Utilization);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private int EstimateTileCount(BestFitResult r, double areaW, double areaL)
|
||||
@@ -410,7 +491,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private int EstimateCount(double pairW, double pairH, double areaW, double areaL)
|
||||
{
|
||||
if (pairW <= 0 || pairH <= 0) return 0;
|
||||
if (pairW <= 0 || pairH <= 0)
|
||||
return 0;
|
||||
var cols = (int)((areaW + partSpacing) / (pairW + partSpacing));
|
||||
var rows = (int)((areaL + partSpacing) / (pairH + partSpacing));
|
||||
return cols * rows * 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -23,7 +23,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
public PartBoundary(Part part, double spacing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer == SpecialLayers.Cut)
|
||||
.ToList();
|
||||
|
||||
@@ -39,21 +40,29 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
// Circumscribe arcs so polygon vertices are always outside
|
||||
// the true arc — guarantees the boundary never under-estimates.
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true);
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(
|
||||
PolygonTolerance,
|
||||
circumscribe: true
|
||||
);
|
||||
polygon.RemoveSelfIntersections();
|
||||
_polygons.Add(polygon);
|
||||
}
|
||||
}
|
||||
|
||||
PrecomputeDirectionalEdges(
|
||||
out _leftEdges, out _rightEdges, out _upEdges, out _downEdges);
|
||||
out _leftEdges,
|
||||
out _rightEdges,
|
||||
out _upEdges,
|
||||
out _downEdges
|
||||
);
|
||||
}
|
||||
|
||||
private void PrecomputeDirectionalEdges(
|
||||
out (Vector start, Vector end)[] leftEdges,
|
||||
out (Vector start, Vector end)[] rightEdges,
|
||||
out (Vector start, Vector end)[] upEdges,
|
||||
out (Vector start, Vector end)[] downEdges)
|
||||
out (Vector start, Vector end)[] downEdges
|
||||
)
|
||||
{
|
||||
var left = new List<(Vector, Vector)>();
|
||||
var right = new List<(Vector, Vector)>();
|
||||
@@ -86,10 +95,14 @@ namespace OpenNest.Engine.Fill
|
||||
var dy = verts[i].Y - verts[i - 1].Y;
|
||||
var edge = (verts[i - 1], verts[i]);
|
||||
|
||||
if (-sign * dy > 0) left.Add(edge);
|
||||
if (sign * dy > 0) right.Add(edge);
|
||||
if (-sign * dx > 0) up.Add(edge);
|
||||
if (sign * dx > 0) down.Add(edge);
|
||||
if (-sign * dy > 0)
|
||||
left.Add(edge);
|
||||
if (sign * dy > 0)
|
||||
right.Add(edge);
|
||||
if (-sign * dx > 0)
|
||||
up.Add(edge);
|
||||
if (sign * dx > 0)
|
||||
down.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +158,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return _leftEdges;
|
||||
case PushDirection.Right: return _rightEdges;
|
||||
case PushDirection.Up: return _upEdges;
|
||||
case PushDirection.Down: return _downEdges;
|
||||
default: return _leftEdges;
|
||||
case PushDirection.Left:
|
||||
return _leftEdges;
|
||||
case PushDirection.Right:
|
||||
return _rightEdges;
|
||||
case PushDirection.Up:
|
||||
return _upEdges;
|
||||
case PushDirection.Down:
|
||||
return _downEdges;
|
||||
default:
|
||||
return _leftEdges;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -31,7 +31,8 @@ namespace OpenNest.Engine.Fill
|
||||
List<NestItem> items,
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null)
|
||||
IProgress<NestProgress> progress = null
|
||||
)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new List<Part>();
|
||||
@@ -60,13 +61,19 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
private static Dictionary<Drawing, int> BuildLocalQuantities(List<NestItem> items)
|
||||
{
|
||||
var localQty = new Dictionary<Drawing, int>(items.Count, ReferenceEqualityComparer.Instance);
|
||||
var localQty = new Dictionary<Drawing, int>(
|
||||
items.Count,
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
foreach (var item in items)
|
||||
localQty[item.Drawing] = item.Quantity;
|
||||
return localQty;
|
||||
}
|
||||
|
||||
private static double FindMinItemDimension(List<NestItem> items, Dictionary<Drawing, int> localQty)
|
||||
private static double FindMinItemDimension(
|
||||
List<NestItem> items,
|
||||
Dictionary<Drawing, int> localQty
|
||||
)
|
||||
{
|
||||
var minDim = double.MaxValue;
|
||||
foreach (var item in items)
|
||||
@@ -87,7 +94,8 @@ namespace OpenNest.Engine.Fill
|
||||
Dictionary<Drawing, int> localQty,
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
List<Part> allParts,
|
||||
CancellationToken token)
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
@@ -147,21 +155,30 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var p in parts)
|
||||
{
|
||||
var bb = p.BoundingBox;
|
||||
if (bb.Left < left) left = bb.Left;
|
||||
if (bb.Bottom < bottom) bottom = bb.Bottom;
|
||||
if (bb.Right > right) right = bb.Right;
|
||||
if (bb.Top > top) top = bb.Top;
|
||||
if (bb.Left < left)
|
||||
left = bb.Left;
|
||||
if (bb.Bottom < bottom)
|
||||
bottom = bb.Bottom;
|
||||
if (bb.Right > right)
|
||||
right = bb.Right;
|
||||
if (bb.Top > top)
|
||||
top = bb.Top;
|
||||
}
|
||||
|
||||
return new Box(left - spacing, bottom - spacing,
|
||||
right - left + spacing * 2, top - bottom + spacing * 2);
|
||||
return new Box(
|
||||
left - spacing,
|
||||
bottom - spacing,
|
||||
right - left + spacing * 2,
|
||||
top - bottom + spacing * 2
|
||||
);
|
||||
}
|
||||
|
||||
private static List<Part> TryFillInRemnants(
|
||||
NestItem item,
|
||||
int qty,
|
||||
List<Box> freeBoxes,
|
||||
Func<NestItem, Box, List<Part>> fillFunc)
|
||||
Func<NestItem, Box, List<Part>> fillFunc
|
||||
)
|
||||
{
|
||||
var itemBbox = item.Drawing.Program.BoundingBox();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -90,12 +90,14 @@ namespace OpenNest.Engine.Fill
|
||||
results.Add(new TieredRemnant(remnant, 1));
|
||||
}
|
||||
|
||||
results.Sort((a, b) =>
|
||||
{
|
||||
if (a.Priority != b.Priority)
|
||||
return a.Priority.CompareTo(b.Priority);
|
||||
return b.Box.Area().CompareTo(a.Box.Area());
|
||||
});
|
||||
results.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
if (a.Priority != b.Priority)
|
||||
return a.Priority.CompareTo(b.Priority);
|
||||
return b.Box.Area().CompareTo(a.Box.Area());
|
||||
}
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -125,11 +127,7 @@ namespace OpenNest.Engine.Fill
|
||||
ys.Add(obs.Top);
|
||||
}
|
||||
|
||||
var grid = new CellGrid
|
||||
{
|
||||
XCoords = xs.ToList(),
|
||||
YCoords = ys.ToList(),
|
||||
};
|
||||
var grid = new CellGrid { XCoords = xs.ToList(), YCoords = ys.ToList() };
|
||||
|
||||
grid.Cols = grid.XCoords.Count - 1;
|
||||
grid.Rows = grid.YCoords.Count - 1;
|
||||
@@ -146,9 +144,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
for (var c = 0; c < grid.Cols; c++)
|
||||
{
|
||||
var cell = new Box(grid.XCoords[c], grid.YCoords[r],
|
||||
var cell = new Box(
|
||||
grid.XCoords[c],
|
||||
grid.YCoords[r],
|
||||
grid.XCoords[c + 1] - grid.XCoords[c],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r]);
|
||||
grid.YCoords[r + 1] - grid.YCoords[r]
|
||||
);
|
||||
|
||||
grid.Empty[r, c] = !OverlapsAny(cell, clipped);
|
||||
}
|
||||
@@ -175,8 +176,12 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
foreach (var obs in obstacles)
|
||||
{
|
||||
if (cell.Left < obs.Right && cell.Right > obs.Left &&
|
||||
cell.Bottom < obs.Top && cell.Top > obs.Bottom)
|
||||
if (
|
||||
cell.Left < obs.Right
|
||||
&& cell.Right > obs.Left
|
||||
&& cell.Bottom < obs.Top
|
||||
&& cell.Top > obs.Bottom
|
||||
)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -227,24 +232,26 @@ namespace OpenNest.Engine.Fill
|
||||
private static bool IsContainedIn(Box inner, Box outer)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
return inner.Left >= outer.Left - eps &&
|
||||
inner.Right <= outer.Right + eps &&
|
||||
inner.Bottom >= outer.Bottom - eps &&
|
||||
inner.Top <= outer.Top + eps;
|
||||
return inner.Left >= outer.Left - eps
|
||||
&& inner.Right <= outer.Right + eps
|
||||
&& inner.Bottom >= outer.Bottom - eps
|
||||
&& inner.Top <= outer.Top + eps;
|
||||
}
|
||||
|
||||
private void SortByEdgeProximity(List<Box> boxes)
|
||||
{
|
||||
boxes.Sort((a, b) =>
|
||||
{
|
||||
var aEdge = TouchesEdge(a) ? 1 : 0;
|
||||
var bEdge = TouchesEdge(b) ? 1 : 0;
|
||||
boxes.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var aEdge = TouchesEdge(a) ? 1 : 0;
|
||||
var bEdge = TouchesEdge(b) ? 1 : 0;
|
||||
|
||||
if (aEdge != bEdge)
|
||||
return bEdge.CompareTo(aEdge);
|
||||
if (aEdge != bEdge)
|
||||
return bEdge.CompareTo(aEdge);
|
||||
|
||||
return b.Area().CompareTo(a.Area());
|
||||
});
|
||||
return b.Area().CompareTo(a.Area());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private bool TouchesEdge(Box box)
|
||||
@@ -264,30 +271,47 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
if (obs.Left < envLeft) envLeft = obs.Left;
|
||||
if (obs.Bottom < envBottom) envBottom = obs.Bottom;
|
||||
if (obs.Right > envRight) envRight = obs.Right;
|
||||
if (obs.Top > envTop) envTop = obs.Top;
|
||||
if (obs.Left < envLeft)
|
||||
envLeft = obs.Left;
|
||||
if (obs.Bottom < envBottom)
|
||||
envBottom = obs.Bottom;
|
||||
if (obs.Right > envRight)
|
||||
envRight = obs.Right;
|
||||
if (obs.Top > envTop)
|
||||
envTop = obs.Top;
|
||||
}
|
||||
|
||||
return new Box(envLeft, envBottom, envRight - envLeft, envTop - envBottom);
|
||||
}
|
||||
|
||||
private static void SplitAtEnvelope(Box remnant, Box envelope, double minDim, List<TieredRemnant> results)
|
||||
private static void SplitAtEnvelope(
|
||||
Box remnant,
|
||||
Box envelope,
|
||||
double minDim,
|
||||
List<TieredRemnant> results
|
||||
)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
|
||||
// Fully within the envelope.
|
||||
if (remnant.Left >= envelope.Left - eps && remnant.Right <= envelope.Right + eps &&
|
||||
remnant.Bottom >= envelope.Bottom - eps && remnant.Top <= envelope.Top + eps)
|
||||
if (
|
||||
remnant.Left >= envelope.Left - eps
|
||||
&& remnant.Right <= envelope.Right + eps
|
||||
&& remnant.Bottom >= envelope.Bottom - eps
|
||||
&& remnant.Top <= envelope.Top + eps
|
||||
)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fully outside the envelope (no overlap).
|
||||
if (remnant.Left >= envelope.Right - eps || remnant.Right <= envelope.Left + eps ||
|
||||
remnant.Bottom >= envelope.Top - eps || remnant.Top <= envelope.Bottom + eps)
|
||||
if (
|
||||
remnant.Left >= envelope.Right - eps
|
||||
|| remnant.Right <= envelope.Left + eps
|
||||
|| remnant.Bottom >= envelope.Top - eps
|
||||
|| remnant.Top <= envelope.Bottom + eps
|
||||
)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 2));
|
||||
return;
|
||||
@@ -300,36 +324,116 @@ namespace OpenNest.Engine.Fill
|
||||
var innerTop = System.Math.Min(remnant.Top, envelope.Top);
|
||||
|
||||
// Inner portion (priority 0).
|
||||
TryAdd(results, innerLeft, innerBottom, innerRight - innerLeft, innerTop - innerBottom, 0, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
innerBottom,
|
||||
innerRight - innerLeft,
|
||||
innerTop - innerBottom,
|
||||
0,
|
||||
minDim
|
||||
);
|
||||
|
||||
// Edge extensions (priority 1).
|
||||
if (remnant.Right > envelope.Right + eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, remnant.Width, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
remnant.Bottom,
|
||||
remnant.Right - envelope.Right,
|
||||
remnant.Width,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, remnant.Width, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
remnant.Bottom,
|
||||
envelope.Left - remnant.Left,
|
||||
remnant.Width,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, innerLeft, envelope.Top, innerRight - innerLeft, remnant.Top - envelope.Top, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
envelope.Top,
|
||||
innerRight - innerLeft,
|
||||
remnant.Top - envelope.Top,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, innerLeft, remnant.Bottom, innerRight - innerLeft, envelope.Bottom - remnant.Bottom, 1, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
innerLeft,
|
||||
remnant.Bottom,
|
||||
innerRight - innerLeft,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
1,
|
||||
minDim
|
||||
);
|
||||
|
||||
// Corner extensions (priority 2).
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, envelope.Right, envelope.Top, remnant.Right - envelope.Right, remnant.Top - envelope.Top, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
envelope.Top,
|
||||
remnant.Right - envelope.Right,
|
||||
remnant.Top - envelope.Top,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
envelope.Right,
|
||||
remnant.Bottom,
|
||||
remnant.Right - envelope.Right,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, remnant.Left, envelope.Top, envelope.Left - remnant.Left, remnant.Top - envelope.Top, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
envelope.Top,
|
||||
envelope.Left - remnant.Left,
|
||||
remnant.Top - envelope.Top,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
TryAdd(
|
||||
results,
|
||||
remnant.Left,
|
||||
remnant.Bottom,
|
||||
envelope.Left - remnant.Left,
|
||||
envelope.Bottom - remnant.Bottom,
|
||||
2,
|
||||
minDim
|
||||
);
|
||||
}
|
||||
|
||||
private static void TryAdd(List<TieredRemnant> results, double x, double y, double w, double h, int priority, double minDim)
|
||||
private static void TryAdd(
|
||||
List<TieredRemnant> results,
|
||||
double x,
|
||||
double y,
|
||||
double w,
|
||||
double h,
|
||||
int priority,
|
||||
double minDim
|
||||
)
|
||||
{
|
||||
if (w >= minDim && h >= minDim)
|
||||
results.Add(new TieredRemnant(new Box(x, y, w, h), priority));
|
||||
@@ -379,10 +483,14 @@ namespace OpenNest.Engine.Fill
|
||||
var top = stack.Pop();
|
||||
startCol = top.startCol;
|
||||
|
||||
candidates.Add(new Box(
|
||||
grid.XCoords[top.startCol], grid.YCoords[r - top.h + 1],
|
||||
grid.XCoords[c] - grid.XCoords[top.startCol],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1]));
|
||||
candidates.Add(
|
||||
new Box(
|
||||
grid.XCoords[top.startCol],
|
||||
grid.YCoords[r - top.h + 1],
|
||||
grid.XCoords[c] - grid.XCoords[top.startCol],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (h > 0)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
@@ -14,7 +14,8 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public static double FindBestRotation(NestItem item)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(item.Drawing.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(item.Drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
@@ -62,7 +63,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public enum ShrinkAxis { Width, Length }
|
||||
public enum ShrinkAxis
|
||||
{
|
||||
Width,
|
||||
Length,
|
||||
}
|
||||
|
||||
public class ShrinkResult
|
||||
{
|
||||
@@ -23,14 +27,16 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public static ShrinkResult Shrink(
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
NestItem item, Box box,
|
||||
NestItem item,
|
||||
Box box,
|
||||
double spacing,
|
||||
ShrinkAxis axis,
|
||||
CancellationToken token = default,
|
||||
int targetCount = 0,
|
||||
IProgress<NestProgress> progress = null,
|
||||
int plateNumber = 0,
|
||||
List<Part> placedParts = null)
|
||||
List<Part> placedParts = null
|
||||
)
|
||||
{
|
||||
var startBox = box;
|
||||
if (targetCount > 0)
|
||||
@@ -38,8 +44,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var parts = fillFunc(item, startBox);
|
||||
|
||||
if (targetCount > 0 && startBox != box
|
||||
&& (parts == null || parts.Count < targetCount))
|
||||
if (targetCount > 0 && startBox != box && (parts == null || parts.Count < targetCount))
|
||||
{
|
||||
parts = fillFunc(item, box);
|
||||
}
|
||||
@@ -47,9 +52,8 @@ namespace OpenNest.Engine.Fill
|
||||
if (parts == null || parts.Count == 0)
|
||||
return new ShrinkResult { Parts = parts ?? new List<Part>(), Dimension = 0 };
|
||||
|
||||
var shrinkTarget = targetCount > 0
|
||||
? System.Math.Min(targetCount, parts.Count)
|
||||
: parts.Count;
|
||||
var shrinkTarget =
|
||||
targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count;
|
||||
|
||||
if (parts.Count > shrinkTarget)
|
||||
parts = TrimToCount(parts, shrinkTarget, axis);
|
||||
@@ -62,16 +66,22 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
private static void ReportShrinkProgress(
|
||||
IProgress<NestProgress> progress, int plateNumber,
|
||||
List<Part> placedParts, List<Part> bestParts,
|
||||
Box workArea, ShrinkAxis axis, double dim)
|
||||
IProgress<NestProgress> progress,
|
||||
int plateNumber,
|
||||
List<Part> placedParts,
|
||||
List<Part> bestParts,
|
||||
Box workArea,
|
||||
ShrinkAxis axis,
|
||||
double dim
|
||||
)
|
||||
{
|
||||
if (progress == null)
|
||||
return;
|
||||
|
||||
var allParts = placedParts != null && placedParts.Count > 0
|
||||
? new List<Part>(placedParts.Count + bestParts.Count)
|
||||
: new List<Part>(bestParts.Count);
|
||||
var allParts =
|
||||
placedParts != null && placedParts.Count > 0
|
||||
? new List<Part>(placedParts.Count + bestParts.Count)
|
||||
: new List<Part>(bestParts.Count);
|
||||
|
||||
if (placedParts != null && placedParts.Count > 0)
|
||||
allParts.AddRange(placedParts);
|
||||
@@ -79,14 +89,17 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
|
||||
|
||||
NestEngineBase.ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = workArea,
|
||||
Description = desc,
|
||||
});
|
||||
NestEngineBase.ReportProgress(
|
||||
progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateNumber,
|
||||
Parts = allParts,
|
||||
WorkArea = workArea,
|
||||
Description = desc,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,8 +107,14 @@ namespace OpenNest.Engine.Fill
|
||||
/// that fits roughly the target count. Scales the shrink axis proportionally
|
||||
/// from the full-area count down to the target, with margin.
|
||||
/// </summary>
|
||||
internal static Box EstimateStartBox(NestItem item, Box box,
|
||||
double spacing, ShrinkAxis axis, int targetCount, double marginFactor = 1.3)
|
||||
internal static Box EstimateStartBox(
|
||||
NestItem item,
|
||||
Box box,
|
||||
double spacing,
|
||||
ShrinkAxis axis,
|
||||
int targetCount,
|
||||
double marginFactor = 1.3
|
||||
)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
if (bbox.Width <= 0 || bbox.Length <= 0)
|
||||
@@ -105,7 +124,10 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
// Use FillBestFit for a fast, accurate rectangle count on the full box.
|
||||
var bin = new Bin { Size = new Size(box.Width, box.Length) };
|
||||
var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) };
|
||||
var packItem = new Item
|
||||
{
|
||||
Size = new Size(bbox.Width + spacing, bbox.Length + spacing),
|
||||
};
|
||||
var packer = new FillBestFit(bin);
|
||||
packer.Fill(packItem);
|
||||
var fullCount = bin.Items.Count;
|
||||
@@ -130,9 +152,7 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
||||
|
||||
return axis == ShrinkAxis.Width
|
||||
? placedBox.Right - box.X
|
||||
: placedBox.Top - box.Y;
|
||||
return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
@@ -7,7 +8,6 @@ using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace OpenNest.Engine.Fill;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class StripeFiller
|
||||
/// Factory to create the engine used for filling the remnant strip.
|
||||
/// Defaults to NestEngineRegistry.Create (uses the user's selected engine).
|
||||
/// </summary>
|
||||
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; }
|
||||
= NestEngineRegistry.Create;
|
||||
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; } =
|
||||
NestEngineRegistry.Create;
|
||||
|
||||
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
||||
{
|
||||
@@ -64,15 +64,27 @@ public class StripeFiller
|
||||
|
||||
foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||
{
|
||||
var perpAxis = axis == NestDirection.Horizontal
|
||||
? NestDirection.Vertical : NestDirection.Horizontal;
|
||||
var perpAxis =
|
||||
axis == NestDirection.Horizontal
|
||||
? NestDirection.Vertical
|
||||
: NestDirection.Horizontal;
|
||||
var sheetSpan = GetDimension(workArea, axis);
|
||||
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
||||
|
||||
var expandResult = ConvergeStripeAngle(
|
||||
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
);
|
||||
var shrinkResult = ConvergeStripeAngleShrink(
|
||||
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
);
|
||||
|
||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||
{
|
||||
@@ -84,9 +96,11 @@ public class StripeFiller
|
||||
if (result == null || result.Count == 0)
|
||||
continue;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " +
|
||||
$"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " +
|
||||
$"grid={result.Count} parts");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] {strategyName} candidate {i} {dirLabel}: "
|
||||
+ $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, "
|
||||
+ $"grid={result.Count} parts"
|
||||
);
|
||||
|
||||
if (_comparer.IsBetter(result, bestParts, workArea))
|
||||
{
|
||||
@@ -95,15 +109,21 @@ public class StripeFiller
|
||||
}
|
||||
}
|
||||
|
||||
_context.ReportProgress(bestParts,
|
||||
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts");
|
||||
_context.ReportProgress(
|
||||
bestParts,
|
||||
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"
|
||||
);
|
||||
}
|
||||
|
||||
return bestParts ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
||||
NestDirection primaryAxis, NestDirection perpAxis)
|
||||
private List<Part> BuildGrid(
|
||||
List<Part> pairParts,
|
||||
double angle,
|
||||
NestDirection primaryAxis,
|
||||
NestDirection perpAxis
|
||||
)
|
||||
{
|
||||
var workArea = _context.WorkArea;
|
||||
var spacing = _context.Plate.PartSpacing;
|
||||
@@ -123,8 +143,10 @@ public class StripeFiller
|
||||
|
||||
var partsPerStripe = stripeParts.Count;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Stripe: {partsPerStripe} parts, " +
|
||||
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] Stripe: {partsPerStripe} parts, "
|
||||
+ $"box={stripeBox.Width:F2}x{stripeBox.Length:F2}"
|
||||
);
|
||||
|
||||
var stripePattern = new Pattern();
|
||||
stripePattern.Parts.AddRange(stripeParts);
|
||||
@@ -141,8 +163,10 @@ public class StripeFiller
|
||||
var completeCount = gridParts.Count / partsPerStripe * partsPerStripe;
|
||||
if (completeCount < gridParts.Count)
|
||||
{
|
||||
Debug.WriteLine($"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " +
|
||||
$"(dropped {gridParts.Count - completeCount} partial)");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} "
|
||||
+ $"(dropped {gridParts.Count - completeCount} partial)"
|
||||
);
|
||||
gridParts = gridParts.GetRange(0, completeCount);
|
||||
}
|
||||
}
|
||||
@@ -184,12 +208,10 @@ public class StripeFiller
|
||||
_context.Item.Drawing,
|
||||
_context.Plate.Size.Length,
|
||||
_context.Plate.Size.Width,
|
||||
_context.Plate.PartSpacing);
|
||||
_context.Plate.PartSpacing
|
||||
);
|
||||
|
||||
return bestFits
|
||||
.Where(r => r.Keep)
|
||||
.Take(MaxPairCandidates)
|
||||
.ToList();
|
||||
return bestFits.Where(r => r.Keep).Take(MaxPairCandidates).ToList();
|
||||
}
|
||||
|
||||
private static Box MakeStripeBox(Box workArea, double perpDim, NestDirection primaryAxis)
|
||||
@@ -208,7 +230,8 @@ public class StripeFiller
|
||||
var gridBox = gridParts.GetBoundingBox();
|
||||
var minDim = System.Math.Min(
|
||||
drawing.Program.BoundingBox().Width,
|
||||
drawing.Program.BoundingBox().Length);
|
||||
drawing.Program.BoundingBox().Length
|
||||
);
|
||||
|
||||
Box remnantBox;
|
||||
|
||||
@@ -229,7 +252,9 @@ public class StripeFiller
|
||||
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Width);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}"
|
||||
);
|
||||
|
||||
var cachedResult = FillResultCache.Get(drawing, remnantBox, spacing);
|
||||
if (cachedResult != null)
|
||||
@@ -246,7 +271,10 @@ public class StripeFiller
|
||||
_context.Token.ThrowIfCancellationRequested();
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => filler.Fill(drawing, angle, dir),
|
||||
null, _comparer, remnantBox);
|
||||
null,
|
||||
_comparer,
|
||||
remnantBox
|
||||
);
|
||||
|
||||
if (result != null && result.Count > (best?.Count ?? 0))
|
||||
best = result;
|
||||
@@ -264,7 +292,10 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
public static double FindAngleForTargetSpan(
|
||||
List<Part> patternParts, double targetSpan, NestDirection axis)
|
||||
List<Part> patternParts,
|
||||
double targetSpan,
|
||||
NestDirection axis
|
||||
)
|
||||
{
|
||||
var bestAngle = 0.0;
|
||||
var bestDiff = double.MaxValue;
|
||||
@@ -292,8 +323,7 @@ public class StripeFiller
|
||||
var (a1, s1) = samples[i];
|
||||
var (a2, s2) = samples[i + 1];
|
||||
|
||||
if ((s1 <= targetSpan && targetSpan <= s2) ||
|
||||
(s2 <= targetSpan && targetSpan <= s1))
|
||||
if ((s1 <= targetSpan && targetSpan <= s2) || (s2 <= targetSpan && targetSpan <= s1))
|
||||
{
|
||||
var result = BisectForTarget(patternParts, a1, a2, targetSpan, axis);
|
||||
var resultSpan = GetRotatedSpan(patternParts, result, axis);
|
||||
@@ -332,8 +362,12 @@ public class StripeFiller
|
||||
/// Returns (angle, waste, pairCount).
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
||||
List<Part> patternParts, double sheetSpan, double spacing,
|
||||
NestDirection axis, CancellationToken token = default)
|
||||
List<Part> patternParts,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var startAngle = OrientShortSideAlong(patternParts, axis);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
||||
@@ -344,8 +378,12 @@ public class StripeFiller
|
||||
/// Complements ConvergeStripeAngle which only expands.
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink(
|
||||
List<Part> patternParts, double sheetSpan, double spacing,
|
||||
NestDirection axis, CancellationToken token = default)
|
||||
List<Part> patternParts,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
||||
var naturalPattern = FillHelpers.BuildRotatedPattern(patternParts, baseAngle);
|
||||
@@ -366,8 +404,13 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
||||
List<Part> patternParts, double startAngle, double sheetSpan,
|
||||
double spacing, NestDirection axis, CancellationToken token)
|
||||
List<Part> patternParts,
|
||||
double startAngle,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var bestWaste = double.MaxValue;
|
||||
var bestAngle = startAngle;
|
||||
@@ -381,15 +424,18 @@ public class StripeFiller
|
||||
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
||||
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
||||
var perpDim = axis == NestDirection.Horizontal
|
||||
? rotated.BoundingBox.Width : rotated.BoundingBox.Length;
|
||||
var perpDim =
|
||||
axis == NestDirection.Horizontal
|
||||
? rotated.BoundingBox.Width
|
||||
: rotated.BoundingBox.Length;
|
||||
|
||||
if (pairSpan + spacing <= 0)
|
||||
break;
|
||||
|
||||
var stripeBox = axis == NestDirection.Horizontal
|
||||
? new Box(0, 0, sheetSpan, perpDim)
|
||||
: new Box(0, 0, perpDim, sheetSpan);
|
||||
var stripeBox =
|
||||
axis == NestDirection.Horizontal
|
||||
? new Box(0, 0, sheetSpan, perpDim)
|
||||
: new Box(0, 0, perpDim, sheetSpan);
|
||||
var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" };
|
||||
var filled = engine.Fill(rotated, axis);
|
||||
var n = filled?.Count ?? 0;
|
||||
@@ -400,8 +446,10 @@ public class StripeFiller
|
||||
var filledBox = ((IEnumerable<IBoundable>)filled).GetBoundingBox();
|
||||
var remaining = sheetSpan - GetDimension(filledBox, axis);
|
||||
|
||||
Debug.WriteLine($"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " +
|
||||
$"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}");
|
||||
Debug.WriteLine(
|
||||
$"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, "
|
||||
+ $"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}"
|
||||
);
|
||||
|
||||
if (remaining < bestWaste)
|
||||
{
|
||||
@@ -414,7 +462,8 @@ public class StripeFiller
|
||||
break;
|
||||
|
||||
var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
||||
if (bboxN <= 0) bboxN = 1;
|
||||
if (bboxN <= 0)
|
||||
bboxN = 1;
|
||||
var delta = remaining / bboxN;
|
||||
var targetSpan = pairSpan + delta;
|
||||
|
||||
@@ -429,8 +478,12 @@ public class StripeFiller
|
||||
}
|
||||
|
||||
private static double BisectForTarget(
|
||||
List<Part> patternParts, double lo, double hi,
|
||||
double targetSpan, NestDirection axis)
|
||||
List<Part> patternParts,
|
||||
double lo,
|
||||
double hi,
|
||||
double targetSpan,
|
||||
NestDirection axis
|
||||
)
|
||||
{
|
||||
var bestAngle = lo;
|
||||
var bestDiff = double.MaxValue;
|
||||
@@ -451,8 +504,10 @@ public class StripeFiller
|
||||
break;
|
||||
|
||||
var loSpan = GetRotatedSpan(patternParts, lo, axis);
|
||||
if ((loSpan < targetSpan && span < targetSpan) ||
|
||||
(loSpan > targetSpan && span > targetSpan))
|
||||
if (
|
||||
(loSpan < targetSpan && span < targetSpan)
|
||||
|| (loSpan > targetSpan && span > targetSpan)
|
||||
)
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
@@ -461,8 +516,7 @@ public class StripeFiller
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
private static double GetRotatedSpan(
|
||||
List<Part> patternParts, double angle, NestDirection axis)
|
||||
private static double GetRotatedSpan(List<Part> patternParts, double angle, NestDirection axis)
|
||||
{
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle);
|
||||
return axis == NestDirection.Horizontal
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill
|
||||
return candExtent < currExtent;
|
||||
|
||||
return FillScore.Compute(candidate, workArea).Density
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
> FillScore.Compute(current, workArea).Density;
|
||||
}
|
||||
|
||||
private static double XExtent(List<Part> parts)
|
||||
@@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var bb = part.BoundingBox;
|
||||
if (bb.Left < minX) minX = bb.Left;
|
||||
if (bb.Right > maxX) maxX = bb.Right;
|
||||
if (bb.Left < minX)
|
||||
minX = bb.Left;
|
||||
if (bb.Right > maxX)
|
||||
maxX = bb.Right;
|
||||
}
|
||||
|
||||
return maxX - minX;
|
||||
|
||||
Reference in New Issue
Block a user