merge: resolve conflicts from remote nesting progress changes
Kept using OpenNest.Api in Timing.cs and EditNestForm.cs alongside remote's reorganized usings and namespace changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds candidate rotation angles for single-item fill. Encapsulates the
|
||||
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
|
||||
/// known-good pruning across fills.
|
||||
/// </summary>
|
||||
public class AngleCandidateBuilder
|
||||
{
|
||||
private readonly HashSet<double> knownGoodAngles = new();
|
||||
|
||||
public bool ForceFullSweep { get; set; }
|
||||
|
||||
public List<double> Build(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
var testPart = new Part(item.Drawing);
|
||||
if (!bestRotation.IsEqualTo(0))
|
||||
testPart.Rotate(bestRotation);
|
||||
testPart.UpdateBounds();
|
||||
|
||||
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
|
||||
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
|
||||
|
||||
if (needsSweep)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
for (var a = 0.0; a < System.Math.PI; a += step)
|
||||
{
|
||||
if (!angles.Any(existing => existing.IsEqualTo(a)))
|
||||
angles.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features != null)
|
||||
{
|
||||
var predicted = AnglePredictor.PredictAngles(
|
||||
features, workArea.Width, workArea.Length);
|
||||
|
||||
if (predicted != null)
|
||||
{
|
||||
var mlAngles = new List<double>(predicted);
|
||||
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
|
||||
mlAngles.Add(bestRotation);
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
|
||||
mlAngles.Add(bestRotation + Angle.HalfPI);
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
|
||||
angles = mlAngles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
|
||||
{
|
||||
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
foreach (var a in knownGoodAngles)
|
||||
{
|
||||
if (!pruned.Any(existing => existing.IsEqualTo(a)))
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
|
||||
return pruned;
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records angles that produced results. These are used to prune
|
||||
/// subsequent Build() calls.
|
||||
/// </summary>
|
||||
public void RecordProductive(List<AngleResult> angleResults)
|
||||
{
|
||||
foreach (var ar in angleResults)
|
||||
{
|
||||
if (ar.PartCount > 0)
|
||||
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit.Tiling;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a group of parts left and down to close gaps after placement.
|
||||
/// Uses the same directional-distance logic as PlateView.PushSelected
|
||||
/// but operates on Part objects directly.
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// Compacts movingParts toward the bottom-left of the plate work area.
|
||||
/// Everything already on the plate (excluding movingParts) is treated
|
||||
/// as stationary obstacles.
|
||||
/// </summary>
|
||||
private const double RepeatThreshold = 0.01;
|
||||
private const int MaxIterations = 20;
|
||||
|
||||
public static void Compact(List<Part> movingParts, Plate plate)
|
||||
{
|
||||
if (movingParts == null || movingParts.Count == 0)
|
||||
return;
|
||||
|
||||
var savedPositions = SavePositions(movingParts);
|
||||
|
||||
// Try left-first.
|
||||
var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||
|
||||
// Restore and try down-first.
|
||||
RestorePositions(movingParts, savedPositions);
|
||||
var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left);
|
||||
|
||||
// Keep left-first if it traveled further.
|
||||
if (leftFirst > downFirst)
|
||||
{
|
||||
RestorePositions(movingParts, savedPositions);
|
||||
CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||
}
|
||||
}
|
||||
|
||||
private static double CompactLoop(List<Part> parts, Plate plate,
|
||||
PushDirection first, PushDirection second)
|
||||
{
|
||||
var total = 0.0;
|
||||
|
||||
for (var i = 0; i < MaxIterations; i++)
|
||||
{
|
||||
var a = Push(parts, plate, first);
|
||||
var b = Push(parts, plate, second);
|
||||
total += a + b;
|
||||
|
||||
if (a <= RepeatThreshold && b <= RepeatThreshold)
|
||||
break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static Vector[] SavePositions(List<Part> parts)
|
||||
{
|
||||
var positions = new Vector[parts.Count];
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
positions[i] = parts[i].Location;
|
||||
return positions;
|
||||
}
|
||||
|
||||
private static void RestorePositions(List<Part> parts, Vector[] positions)
|
||||
{
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
parts[i].Location = positions[i];
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
|
||||
}
|
||||
|
||||
/// <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, double angle)
|
||||
{
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
var opposite = -direction;
|
||||
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var halfSpacing = partSpacing / 2;
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = direction * distance;
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var halfSpacing = partSpacing / 2;
|
||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||
var distance = double.MaxValue;
|
||||
|
||||
// BB gap at which offset geometries are expected to be touching.
|
||||
var contactGap = (halfSpacing + ChordTolerance) * 2;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
// Use the reverse-direction gap to check if the obstacle is entirely
|
||||
// behind the moving part. The forward gap (gap < 0) is unreliable for
|
||||
// irregular shapes whose bounding boxes overlap even when the actual
|
||||
// geometry still has a valid contact in the push direction.
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = SpatialQuery.DirectionToOffset(direction, distance);
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts using bounding-box distances only (no geometry lines).
|
||||
/// 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)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
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)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing;
|
||||
if (d < 0) d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = SpatialQuery.DirectionToOffset(direction, distance);
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compacts parts individually toward the bottom-left of the work area.
|
||||
/// Each part is pushed against all others as obstacles, closing geometry-based gaps.
|
||||
/// Does not require parts to be on a plate.
|
||||
/// </summary>
|
||||
public static void CompactIndividual(List<Part> parts, Box workArea, double partSpacing)
|
||||
{
|
||||
if (parts == null || parts.Count < 2)
|
||||
return;
|
||||
|
||||
var savedPositions = SavePositions(parts);
|
||||
|
||||
var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
|
||||
RestorePositions(parts, savedPositions);
|
||||
var downFirst = CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Down, PushDirection.Left);
|
||||
|
||||
if (leftFirst > downFirst)
|
||||
{
|
||||
RestorePositions(parts, savedPositions);
|
||||
CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
}
|
||||
}
|
||||
|
||||
private static double CompactIndividualLoop(List<Part> parts, Box workArea,
|
||||
double partSpacing, PushDirection first, PushDirection second)
|
||||
{
|
||||
var total = 0.0;
|
||||
|
||||
for (var pass = 0; pass < MaxIterations; pass++)
|
||||
{
|
||||
var moved = 0.0;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var single = new List<Part>(1) { part };
|
||||
var obstacles = new List<Part>(parts.Count - 1);
|
||||
foreach (var p in parts)
|
||||
if (p != part) obstacles.Add(p);
|
||||
|
||||
moved += Push(single, obstacles, workArea, partSpacing, first);
|
||||
moved += Push(single, obstacles, workArea, partSpacing, second);
|
||||
}
|
||||
|
||||
total += moved;
|
||||
if (moved <= RepeatThreshold)
|
||||
break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.RectanglePacking;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -56,7 +55,8 @@ namespace OpenNest
|
||||
if (item.Quantity > 0 && best.Count > item.Quantity)
|
||||
best = best.Take(item.Quantity).ToList();
|
||||
|
||||
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(),
|
||||
isOverallBest: true);
|
||||
|
||||
return best;
|
||||
}
|
||||
@@ -67,89 +67,24 @@ namespace OpenNest
|
||||
if (groupParts == null || groupParts.Count == 0)
|
||||
return new List<Part>();
|
||||
|
||||
// Single part: delegate to the strategy pipeline.
|
||||
if (groupParts.Count == 1)
|
||||
{
|
||||
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
|
||||
return Fill(nestItem, workArea, progress, token);
|
||||
}
|
||||
|
||||
// Multi-part group: linear pattern fill only.
|
||||
PhaseResults.Clear();
|
||||
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
||||
var best = FillPattern(engine, groupParts, angles, workArea);
|
||||
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea);
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||
|
||||
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
|
||||
if (groupParts.Count == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
|
||||
var binItem = BinConverter.ToItem(nestItem, Plate.PartSpacing);
|
||||
var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing);
|
||||
var rectEngine = new FillBestFit(bin);
|
||||
rectEngine.Fill(binItem);
|
||||
var rectResult = BinConverter.ToParts(bin, new List<NestItem> { nestItem });
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.RectBestFit, rectResult?.Count ?? 0, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
|
||||
|
||||
if (IsBetterFill(rectResult, best, workArea))
|
||||
{
|
||||
best = rectResult;
|
||||
ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
|
||||
var pairResult = pairFiller.Fill(nestItem, workArea, PlateNumber, token, progress);
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
|
||||
|
||||
if (IsBetterFill(pairResult, best, workArea))
|
||||
{
|
||||
best = pairResult;
|
||||
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var extentsFiller = new FillExtents(workArea, Plate.PartSpacing);
|
||||
var bestFits2 = BestFitCache.GetOrCompute(
|
||||
groupParts[0].BaseDrawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
var extentsAngles2 = new[] { groupParts[0].Rotation, groupParts[0].Rotation + Angle.HalfPI };
|
||||
List<Part> bestExtents2 = null;
|
||||
|
||||
foreach (var angle in extentsAngles2)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var result = extentsFiller.Fill(groupParts[0].BaseDrawing, angle, PlateNumber, token, progress, bestFits2);
|
||||
if (result != null && result.Count > (bestExtents2?.Count ?? 0))
|
||||
bestExtents2 = result;
|
||||
}
|
||||
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents2?.Count ?? 0, 0));
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Extents: {bestExtents2?.Count ?? 0} parts");
|
||||
|
||||
if (IsBetterFill(bestExtents2, best, workArea))
|
||||
{
|
||||
best = bestExtents2;
|
||||
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
|
||||
}
|
||||
}
|
||||
|
||||
// Always report the final winner so the UI's temporary parts
|
||||
// match the returned result.
|
||||
var winPhase = PhaseResults.Count > 0
|
||||
? PhaseResults.OrderByDescending(r => r.PartCount).First().Phase
|
||||
: NestPhase.Linear;
|
||||
ReportProgress(progress, winPhase, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(),
|
||||
isOverallBest: true);
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
@@ -201,8 +136,13 @@ namespace OpenNest
|
||||
context.CurrentBest = result;
|
||||
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
|
||||
context.WinnerPhase = strategy.Phase;
|
||||
ReportProgress(context.Progress, strategy.Phase, PlateNumber,
|
||||
result, context.WorkArea, BuildProgressSummary());
|
||||
}
|
||||
|
||||
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
{
|
||||
ReportProgress(context.Progress, context.WinnerPhase, PlateNumber,
|
||||
context.CurrentBest, context.WorkArea, BuildProgressSummary(),
|
||||
isOverallBest: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,13 +154,5 @@ namespace OpenNest
|
||||
angleBuilder.RecordProductive(context.AngleResults);
|
||||
}
|
||||
|
||||
// --- Pattern helpers ---
|
||||
|
||||
internal static Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
|
||||
=> FillHelpers.BuildRotatedPattern(groupParts, angle);
|
||||
|
||||
internal static List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
|
||||
=> FillHelpers.FillPattern(engine, groupParts, angles, workArea);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps an IProgress to prepend previously placed parts to each report,
|
||||
@@ -0,0 +1,114 @@
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds candidate rotation angles for single-item fill. Encapsulates the
|
||||
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
|
||||
/// known-good pruning across fills.
|
||||
/// </summary>
|
||||
public class AngleCandidateBuilder
|
||||
{
|
||||
private readonly HashSet<double> knownGoodAngles = new();
|
||||
|
||||
public bool ForceFullSweep { get; set; }
|
||||
|
||||
public List<double> Build(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
|
||||
return BuildPrunedList(baseAngles);
|
||||
|
||||
var angles = new List<double>(baseAngles);
|
||||
|
||||
if (NeedsSweep(item, bestRotation, workArea))
|
||||
AddSweepAngles(angles);
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
angles = ApplyMlPrediction(item, workArea, baseAngles, angles);
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
private bool NeedsSweep(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var testPart = new Part(item.Drawing);
|
||||
if (!bestRotation.IsEqualTo(0))
|
||||
testPart.Rotate(bestRotation);
|
||||
testPart.UpdateBounds();
|
||||
|
||||
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
|
||||
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
return workAreaShortSide < partLongestSide || ForceFullSweep;
|
||||
}
|
||||
|
||||
private static void AddSweepAngles(List<double> angles)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
for (var a = 0.0; a < System.Math.PI; a += step)
|
||||
{
|
||||
if (!ContainsAngle(angles, a))
|
||||
angles.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<double> ApplyMlPrediction(
|
||||
NestItem item, Box workArea, double[] baseAngles, List<double> fallback)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features == null)
|
||||
return fallback;
|
||||
|
||||
var predicted = AnglePredictor.PredictAngles(features, workArea.Width, workArea.Length);
|
||||
if (predicted == null)
|
||||
return fallback;
|
||||
|
||||
var mlAngles = new List<double>(predicted);
|
||||
foreach (var b in baseAngles)
|
||||
{
|
||||
if (!ContainsAngle(mlAngles, b))
|
||||
mlAngles.Add(b);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted");
|
||||
return mlAngles;
|
||||
}
|
||||
|
||||
private List<double> BuildPrunedList(double[] baseAngles)
|
||||
{
|
||||
var pruned = new List<double>(baseAngles);
|
||||
foreach (var a in knownGoodAngles)
|
||||
{
|
||||
if (!ContainsAngle(pruned, a))
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
|
||||
return pruned;
|
||||
}
|
||||
|
||||
private static bool ContainsAngle(List<double> angles, double angle)
|
||||
{
|
||||
return angles.Any(existing => existing.IsEqualTo(angle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records angles that produced results. These are used to prune
|
||||
/// subsequent Build() calls.
|
||||
/// </summary>
|
||||
public void RecordProductive(List<AngleResult> angleResults)
|
||||
{
|
||||
foreach (var ar in angleResults)
|
||||
{
|
||||
if (ar.PartCount > 0)
|
||||
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -0,0 +1,178 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a group of parts left and down to close gaps after placement.
|
||||
/// Uses the same directional-distance logic as PlateView.PushSelected
|
||||
/// but operates on Part objects directly.
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var opposite = -direction;
|
||||
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var halfSpacing = partSpacing / 2;
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = direction * distance;
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts using bounding-box distances only (no geometry lines).
|
||||
/// 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)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
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)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing;
|
||||
if (d < 0) d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = SpatialQuery.DirectionToOffset(direction, distance);
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public class FillExtents
|
||||
{
|
||||
@@ -173,18 +173,12 @@ namespace OpenNest
|
||||
if (minSlide >= double.MaxValue || minSlide < 0)
|
||||
return pairHeight + partSpacing;
|
||||
|
||||
// Boundaries are inflated by halfSpacing, so when inflated edges touch
|
||||
// the actual parts have partSpacing gap. Match FillLinear's pattern:
|
||||
// startOffset = pairHeight (no extra spacing), copyDist = height - slide.
|
||||
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
|
||||
// clamped so it never goes below pairHeight + partSpacing to prevent
|
||||
// bounding-box overlap from spurious slide values.
|
||||
var copyDist = pairHeight - minSlide;
|
||||
|
||||
// Boundaries are inflated by halfSpacing, so the geometry-aware
|
||||
// distance already guarantees partSpacing gap. Only fall back to
|
||||
// bounding-box distance if the calculation produced a non-positive value.
|
||||
if (copyDist <= Tolerance.Epsilon)
|
||||
return pairHeight + partSpacing;
|
||||
|
||||
return copyDist;
|
||||
return System.Math.Max(copyDist, pairHeight + partSpacing);
|
||||
}
|
||||
|
||||
private static double SlideDistance(
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public class FillLinear
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace OpenNest
|
||||
public Box WorkArea { get; }
|
||||
|
||||
public double PartSpacing { get; }
|
||||
|
||||
|
||||
public double HalfSpacing => PartSpacing / 2;
|
||||
|
||||
/// <summary>
|
||||
@@ -110,47 +110,40 @@ namespace OpenNest
|
||||
var pushDir = GetPushDirection(direction);
|
||||
var opposite = SpatialQuery.OppositeDirection(pushDir);
|
||||
|
||||
// Compute a starting offset large enough that every part-pair in
|
||||
// patternB has its offset geometry beyond patternA's offset geometry.
|
||||
var maxUpper = double.MinValue;
|
||||
var minLower = double.MaxValue;
|
||||
|
||||
for (var i = 0; i < patternA.Parts.Count; i++)
|
||||
{
|
||||
var bb = patternA.Parts[i].BoundingBox;
|
||||
var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top;
|
||||
var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom;
|
||||
|
||||
if (upper > maxUpper) maxUpper = upper;
|
||||
if (lower < minLower) minLower = lower;
|
||||
}
|
||||
|
||||
var startOffset = System.Math.Max(bboxDim,
|
||||
maxUpper - minLower + PartSpacing + Tolerance.Epsilon);
|
||||
|
||||
// bboxDim already spans max(upper) - min(lower) across all parts,
|
||||
// so the start offset just needs to push beyond that plus spacing.
|
||||
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
||||
var offset = MakeOffset(direction, startOffset);
|
||||
|
||||
// Pre-cache edge arrays.
|
||||
var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
|
||||
var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
|
||||
var maxCopyDistance = FindMaxPairDistance(
|
||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
||||
|
||||
for (var i = 0; i < patternA.Parts.Count; i++)
|
||||
{
|
||||
movingEdges[i] = boundaries[i].GetEdges(pushDir);
|
||||
stationaryEdges[i] = boundaries[i].GetEdges(opposite);
|
||||
}
|
||||
if (maxCopyDistance < Tolerance.Epsilon)
|
||||
return bboxDim + PartSpacing;
|
||||
|
||||
return maxCopyDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests every pair of parts across adjacent pattern copies and returns the
|
||||
/// maximum copy distance found. Returns 0 if no valid slide was found.
|
||||
/// </summary>
|
||||
private static double FindMaxPairDistance(
|
||||
List<Part> parts, PartBoundary[] boundaries, Vector offset,
|
||||
PushDirection pushDir, PushDirection opposite, double startOffset)
|
||||
{
|
||||
var maxCopyDistance = 0.0;
|
||||
|
||||
for (var j = 0; j < patternA.Parts.Count; j++)
|
||||
for (var j = 0; j < parts.Count; j++)
|
||||
{
|
||||
var locationB = patternA.Parts[j].Location + offset;
|
||||
var movingEdges = boundaries[j].GetEdges(pushDir);
|
||||
var locationB = parts[j].Location + offset;
|
||||
|
||||
for (var i = 0; i < patternA.Parts.Count; i++)
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEdges[j], locationB,
|
||||
stationaryEdges[i], patternA.Parts[i].Location,
|
||||
movingEdges, locationB,
|
||||
boundaries[i].GetEdges(opposite), parts[i].Location,
|
||||
pushDir);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
@@ -163,9 +156,6 @@ namespace OpenNest
|
||||
}
|
||||
}
|
||||
|
||||
if (maxCopyDistance < Tolerance.Epsilon)
|
||||
return bboxDim + PartSpacing;
|
||||
|
||||
return maxCopyDistance;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public readonly struct FillScore : System.IComparable<FillScore>
|
||||
{
|
||||
@@ -1,23 +1,35 @@
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Fills a work area using interlocking part pairs from BestFitCache.
|
||||
/// Extracted from DefaultNestEngine.FillWithPairs.
|
||||
/// </summary>
|
||||
public class PairFiller
|
||||
{
|
||||
private const int MaxTopCandidates = 50;
|
||||
private const int MaxStripCandidates = 100;
|
||||
private const double MinStripUtilization = 0.3;
|
||||
private const int EarlyExitMinTried = 10;
|
||||
private const int EarlyExitStaleLimit = 10;
|
||||
|
||||
private readonly Size plateSize;
|
||||
private readonly double partSpacing;
|
||||
|
||||
/// <summary>
|
||||
/// The best-fit results computed during the last Fill call.
|
||||
/// Available after Fill returns so callers can reuse without recomputing.
|
||||
/// </summary>
|
||||
public List<BestFitResult> BestFits { get; private set; }
|
||||
|
||||
public PairFiller(Size plateSize, double partSpacing)
|
||||
{
|
||||
this.plateSize = plateSize;
|
||||
@@ -29,11 +41,11 @@ namespace OpenNest
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
BestFits = BestFitCache.GetOrCompute(
|
||||
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}");
|
||||
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}");
|
||||
|
||||
List<Part> best = null;
|
||||
@@ -46,17 +58,7 @@ namespace OpenNest
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var result = candidates[i];
|
||||
var pairParts = result.BuildParts(item.Drawing);
|
||||
var angles = result.HullAngles;
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
|
||||
// Let the remainder strip try pair-based filling too.
|
||||
var p0 = DefaultNestEngine.BuildRotatedPattern(pairParts, 0);
|
||||
var p90 = DefaultNestEngine.BuildRotatedPattern(pairParts, Angle.HalfPI);
|
||||
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
|
||||
|
||||
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea);
|
||||
var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea);
|
||||
|
||||
if (filled != null && filled.Count > 0)
|
||||
{
|
||||
@@ -80,8 +82,7 @@ namespace OpenNest
|
||||
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
|
||||
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
|
||||
|
||||
// Early exit: stop if we've tried enough candidates without improvement.
|
||||
if (i >= 9 && sinceImproved >= 10)
|
||||
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||
break;
|
||||
@@ -97,10 +98,22 @@ namespace OpenNest
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea)
|
||||
{
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
|
||||
var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0);
|
||||
var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI);
|
||||
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
|
||||
|
||||
return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea);
|
||||
}
|
||||
|
||||
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
||||
{
|
||||
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||
var top = kept.Take(50).ToList();
|
||||
var top = kept.Take(MaxTopCandidates).ToList();
|
||||
|
||||
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
||||
@@ -109,14 +122,14 @@ namespace OpenNest
|
||||
{
|
||||
var stripCandidates = bestFits
|
||||
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= 0.3)
|
||||
&& r.Utilization >= MinStripUtilization)
|
||||
.OrderByDescending(r => r.Utilization);
|
||||
|
||||
var existing = new HashSet<BestFitResult>(top);
|
||||
|
||||
foreach (var r in stripCandidates)
|
||||
{
|
||||
if (top.Count >= 100)
|
||||
if (top.Count >= MaxStripCandidates)
|
||||
break;
|
||||
|
||||
if (existing.Add(r))
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Pre-computed offset boundary polygons for a part's geometry.
|
||||
@@ -87,9 +87,9 @@ namespace OpenNest
|
||||
var edge = (verts[i - 1], verts[i]);
|
||||
|
||||
if (-sign * dy > 0) left.Add(edge);
|
||||
if ( sign * dy > 0) right.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 * dx > 0) down.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,11 @@ namespace OpenNest
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: 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;
|
||||
case PushDirection.Up: return _upEdges;
|
||||
case PushDirection.Down: return _downEdges;
|
||||
default: return _leftEdges;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public class Pattern
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public static class PatternTiler
|
||||
{
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Iteratively fills remnant boxes with items using a RemnantFinder.
|
||||
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// A remnant box with a priority tier.
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
internal static class RotationAnalysis
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public enum ShrinkAxis { Width, Height }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.ML
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.ML
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.ML
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -190,7 +191,8 @@ namespace OpenNest
|
||||
int plateNumber,
|
||||
List<Part> best,
|
||||
Box workArea,
|
||||
string description)
|
||||
string description,
|
||||
bool isOverallBest = false)
|
||||
{
|
||||
if (progress == null || best == null || best.Count == 0)
|
||||
return;
|
||||
@@ -212,9 +214,13 @@ namespace OpenNest
|
||||
$"PartArea={totalPartArea:F0}, Remnant={workArea.Area() - totalPartArea:F0}, " +
|
||||
$"WorkArea={workArea.Width:F1}x{workArea.Length:F1} | {description}";
|
||||
Debug.WriteLine(msg);
|
||||
try { System.IO.File.AppendAllText(
|
||||
try
|
||||
{
|
||||
System.IO.File.AppendAllText(
|
||||
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } catch { }
|
||||
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n");
|
||||
}
|
||||
catch { }
|
||||
|
||||
progress.Report(new NestProgress
|
||||
{
|
||||
@@ -228,6 +234,7 @@ namespace OpenNest
|
||||
BestParts = clonedParts,
|
||||
Description = description,
|
||||
ActiveWorkArea = workArea,
|
||||
IsOverallBest = isOverallBest,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -46,5 +46,6 @@ namespace OpenNest
|
||||
public List<Part> BestParts { get; set; }
|
||||
public string Description { get; set; }
|
||||
public Box ActiveWorkArea { get; set; }
|
||||
public bool IsOverallBest { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
/// <summary>
|
||||
/// Mixed-part geometry-aware nesting using NFP-based collision avoidance
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
/// <summary>
|
||||
/// NFP-based Bottom-Left Fill (BLF) placement engine.
|
||||
@@ -1,8 +1,9 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of a nest optimization run.
|
||||
@@ -1,8 +1,8 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
/// <summary>
|
||||
/// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB).
|
||||
@@ -1,11 +1,12 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulated annealing optimizer for NFP-based nesting.
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.RapidPlanning;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.RapidPlanning;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.RapidPlanning
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
@@ -71,7 +71,7 @@ namespace OpenNest.RectanglePacking
|
||||
|
||||
if (pt.X != double.MaxValue && pt.Y != double.MaxValue)
|
||||
return pt;
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.RectanglePacking
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Sequencing
|
||||
{
|
||||
|
||||
@@ -25,10 +25,10 @@ namespace OpenNest.Engine.Sequencing
|
||||
|
||||
private static double MinEdgeDistance(OpenNest.Geometry.Vector center, OpenNest.Geometry.Box plateBox)
|
||||
{
|
||||
var distLeft = center.X - plateBox.Left;
|
||||
var distRight = plateBox.Right - center.X;
|
||||
var distLeft = center.X - plateBox.Left;
|
||||
var distRight = plateBox.Right - center.X;
|
||||
var distBottom = center.Y - plateBox.Bottom;
|
||||
var distTop = plateBox.Top - center.Y;
|
||||
var distTop = plateBox.Top - center.Y;
|
||||
|
||||
return System.Math.Min(System.Math.Min(distLeft, distRight), System.Math.Min(distBottom, distTop));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.Sequencing
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Engine.Sequencing
|
||||
{
|
||||
@@ -9,12 +9,12 @@ namespace OpenNest.Engine.Sequencing
|
||||
{
|
||||
return parameters.Method switch
|
||||
{
|
||||
SequenceMethod.RightSide => new RightSideSequencer(),
|
||||
SequenceMethod.LeftSide => new LeftSideSequencer(),
|
||||
SequenceMethod.RightSide => new RightSideSequencer(),
|
||||
SequenceMethod.LeftSide => new LeftSideSequencer(),
|
||||
SequenceMethod.BottomSide => new BottomSideSequencer(),
|
||||
SequenceMethod.EdgeStart => new EdgeStartSequencer(),
|
||||
SequenceMethod.LeastCode => new LeastCodeSequencer(),
|
||||
SequenceMethod.Advanced => new AdvancedSequencer(parameters),
|
||||
SequenceMethod.EdgeStart => new EdgeStartSequencer(),
|
||||
SequenceMethod.LeastCode => new LeastCodeSequencer(),
|
||||
SequenceMethod.Advanced => new AdvancedSequencer(parameters),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Sequence method '{parameters.Method}' is not supported.")
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class ExtentsFillStrategy : IFillStrategy
|
||||
{
|
||||
@@ -20,10 +20,6 @@ namespace OpenNest
|
||||
|
||||
var angles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
var bestFits = context.SharedState.TryGetValue("BestFits", out var cached)
|
||||
? (List<BestFitResult>)cached
|
||||
: null;
|
||||
|
||||
List<Part> best = null;
|
||||
var bestScore = default(FillScore);
|
||||
|
||||
@@ -31,7 +27,7 @@ namespace OpenNest
|
||||
{
|
||||
context.Token.ThrowIfCancellationRequested();
|
||||
var result = filler.Fill(context.Item.Drawing, angle,
|
||||
context.PlateNumber, context.Token, context.Progress, bestFits);
|
||||
context.PlateNumber, context.Token, context.Progress);
|
||||
if (result != null && result.Count > 0)
|
||||
{
|
||||
var score = FillScore.Compute(result, context.WorkArea);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class FillContext
|
||||
{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public static class FillHelpers
|
||||
{
|
||||
|
||||
@@ -5,12 +5,13 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public static class FillStrategyRegistry
|
||||
{
|
||||
private static readonly List<IFillStrategy> strategies = new();
|
||||
private static List<IFillStrategy> sorted;
|
||||
private static HashSet<string> enabledFilter;
|
||||
|
||||
static FillStrategyRegistry()
|
||||
{
|
||||
@@ -18,7 +19,21 @@ namespace OpenNest
|
||||
}
|
||||
|
||||
public static IReadOnlyList<IFillStrategy> Strategies =>
|
||||
sorted ??= strategies.OrderBy(s => s.Order).ToList();
|
||||
sorted ??= (enabledFilter != null
|
||||
? strategies.Where(s => enabledFilter.Contains(s.Name)).OrderBy(s => s.Order).ToList()
|
||||
: strategies.OrderBy(s => s.Order).ToList());
|
||||
|
||||
/// <summary>
|
||||
/// Restricts the active strategies to only those whose names are listed.
|
||||
/// Pass null to restore all strategies.
|
||||
/// </summary>
|
||||
public static void SetEnabled(params string[] names)
|
||||
{
|
||||
enabledFilter = names != null && names.Length > 0
|
||||
? new HashSet<string>(names, StringComparer.OrdinalIgnoreCase)
|
||||
: null;
|
||||
sorted = null;
|
||||
}
|
||||
|
||||
public static void LoadFrom(Assembly assembly)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public interface IFillStrategy
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class LinearFillStrategy : IFillStrategy
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.BestFit;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class PairsFillStrategy : IFillStrategy
|
||||
{
|
||||
@@ -15,11 +15,7 @@ namespace OpenNest
|
||||
var result = filler.Fill(context.Item, context.WorkArea,
|
||||
context.PlateNumber, context.Token, context.Progress);
|
||||
|
||||
// Cache hit — PairFiller already called GetOrCompute internally.
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
context.Item.Drawing, context.Plate.Size.Length,
|
||||
context.Plate.Size.Width, context.Plate.PartSpacing);
|
||||
context.SharedState["BestFits"] = bestFits;
|
||||
context.SharedState["BestFits"] = filler.BestFits;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class RectBestFitStrategy : IFillStrategy
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -182,16 +183,6 @@ namespace OpenNest
|
||||
var bestParts = shrinkResult.Parts;
|
||||
var bestDim = shrinkResult.Dimension;
|
||||
|
||||
// TODO: Compact strip parts individually to close geometry-based gaps.
|
||||
// Disabled pending investigation — remnant finder picks up gaps created
|
||||
// by compaction and scatters parts into them.
|
||||
// Compactor.CompactIndividual(bestParts, workArea, Plate.PartSpacing);
|
||||
//
|
||||
// var compactedBox = bestParts.Cast<IBoundable>().GetBoundingBox();
|
||||
// bestDim = direction == StripDirection.Bottom
|
||||
// ? compactedBox.Top - workArea.Y
|
||||
// : compactedBox.Right - workArea.X;
|
||||
|
||||
// Build remnant box with spacing gap.
|
||||
var spacing = Plate.PartSpacing;
|
||||
var remnantBox = direction == StripDirection.Bottom
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user