Merge branch 'master' of https://git.thecozycat.net/aj/OpenNest
This commit is contained in:
+35
-30
@@ -119,6 +119,9 @@ namespace OpenNest
|
|||||||
|
|
||||||
Size = new Size(Size.Length, Size.Width);
|
Size = new Size(Size.Length, Size.Width);
|
||||||
|
|
||||||
|
// After Size swap above, new Size.Width = old Length (old X extent),
|
||||||
|
// new Size.Length = old Width (old Y extent).
|
||||||
|
// Convention: Length = X axis, Width = Y axis.
|
||||||
if (rotationDirection == RotationType.CW)
|
if (rotationDirection == RotationType.CW)
|
||||||
{
|
{
|
||||||
Rotate(oneAndHalfPI);
|
Rotate(oneAndHalfPI);
|
||||||
@@ -128,19 +131,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
Offset(0, Size.Length);
|
Offset(0, Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
Offset(-Size.Width, 0);
|
Offset(-Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
Offset(0, -Size.Length);
|
Offset(0, -Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
Offset(Size.Width, 0);
|
Offset(Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -161,19 +164,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
Offset(Size.Width, 0);
|
Offset(Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
Offset(0, Size.Length);
|
Offset(0, Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
Offset(-Size.Width, 0);
|
Offset(-Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
Offset(0, -Size.Length);
|
Offset(0, -Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -200,19 +203,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
centerpt = new Vector(Size.Width * 0.5, Size.Length * 0.5);
|
centerpt = new Vector(Size.Length * 0.5, Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, Size.Length * 0.5);
|
centerpt = new Vector(-Size.Length * 0.5, Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, -Size.Length * 0.5);
|
centerpt = new Vector(-Size.Length * 0.5, -Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
centerpt = new Vector(Size.Width * 0.5, -Size.Length * 0.5);
|
centerpt = new Vector(Size.Length * 0.5, -Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -294,6 +297,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var plateBox = new Box();
|
var plateBox = new Box();
|
||||||
|
|
||||||
|
// Convention: Size.Length = X axis (horizontal), Size.Width = Y axis (vertical)
|
||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
@@ -302,26 +306,26 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
plateBox.X = (float)-Size.Width;
|
plateBox.X = (float)-Size.Length;
|
||||||
plateBox.Y = 0;
|
plateBox.Y = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
plateBox.X = (float)-Size.Width;
|
plateBox.X = (float)-Size.Length;
|
||||||
plateBox.Y = (float)-Size.Length;
|
plateBox.Y = (float)-Size.Width;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
plateBox.X = 0;
|
plateBox.X = 0;
|
||||||
plateBox.Y = (float)-Size.Length;
|
plateBox.Y = (float)-Size.Width;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new Box();
|
return new Box();
|
||||||
}
|
}
|
||||||
|
|
||||||
plateBox.Width = Size.Width;
|
plateBox.Width = Size.Length;
|
||||||
plateBox.Length = Size.Length;
|
plateBox.Length = Size.Width;
|
||||||
|
|
||||||
if (!includeParts)
|
if (!includeParts)
|
||||||
return plateBox;
|
return plateBox;
|
||||||
@@ -382,29 +386,30 @@ namespace OpenNest
|
|||||||
|
|
||||||
var bounds = Parts.GetBoundingBox();
|
var bounds = Parts.GetBoundingBox();
|
||||||
|
|
||||||
double width;
|
// Convention: Length = X axis, Width = Y axis
|
||||||
double length;
|
double xExtent;
|
||||||
|
double yExtent;
|
||||||
|
|
||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -412,8 +417,8 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
Size = new Size(
|
Size = new Size(
|
||||||
Rounding.RoundUpToNearest(width, roundingFactor),
|
Rounding.RoundUpToNearest(yExtent, roundingFactor),
|
||||||
Rounding.RoundUpToNearest(length, roundingFactor));
|
Rounding.RoundUpToNearest(xExtent, roundingFactor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ namespace OpenNest.Engine.BestFit.Tiling
|
|||||||
{
|
{
|
||||||
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
|
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
|
||||||
{
|
{
|
||||||
var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
|
var plateWidth = plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
|
||||||
var plateHeight = plate.Size.Length - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
|
var plateHeight = plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
|
||||||
|
|
||||||
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
|
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
|
||||||
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
|
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
public override string Name => "Default";
|
public override string Name => "Default";
|
||||||
|
|
||||||
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit)";
|
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)";
|
||||||
|
|
||||||
private readonly AngleCandidateBuilder angleBuilder = new();
|
private readonly AngleCandidateBuilder angleBuilder = new();
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
// Top pair candidates — check if pairs tile better in this box.
|
// Top pair candidates — check if pairs tile better in this box.
|
||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
drawing, Plate.Size.Width, Plate.Size.Length, Plate.PartSpacing);
|
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
var topPairs = bestFits.Where(r => r.Keep).Take(3);
|
var topPairs = bestFits.Where(r => r.Keep).Take(3);
|
||||||
|
|
||||||
foreach (var pair in topPairs)
|
foreach (var pair in topPairs)
|
||||||
@@ -146,6 +146,29 @@ namespace OpenNest
|
|||||||
best = pairResult;
|
best = pairResult;
|
||||||
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
|
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var extentsFiller = new FillExtents(workArea, 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);
|
||||||
|
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)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -263,6 +286,34 @@ namespace OpenNest
|
|||||||
WinnerPhase = NestPhase.RectBestFit;
|
WinnerPhase = NestPhase.RectBestFit;
|
||||||
ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
|
ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extents phase
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var extentsSw = Stopwatch.StartNew();
|
||||||
|
var extentsFiller = new FillExtents(workArea, Plate.PartSpacing);
|
||||||
|
List<Part> bestExtents = null;
|
||||||
|
var extentsAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||||
|
|
||||||
|
foreach (var angle in extentsAngles)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var extentsResult = extentsFiller.Fill(item.Drawing, angle, PlateNumber, token, progress);
|
||||||
|
if (bestExtents == null || (extentsResult != null && extentsResult.Count > (bestExtents?.Count ?? 0)))
|
||||||
|
bestExtents = extentsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
extentsSw.Stop();
|
||||||
|
var extentsScore = bestExtents != null ? FillScore.Compute(bestExtents, workArea) : default;
|
||||||
|
Debug.WriteLine($"[FindBestFill] Extents: {extentsScore.Count} parts");
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents?.Count ?? 0, extentsSw.ElapsedMilliseconds));
|
||||||
|
|
||||||
|
var bestScore2 = FillScore.Compute(best, workArea);
|
||||||
|
if (extentsScore > bestScore2)
|
||||||
|
{
|
||||||
|
best = bestExtents;
|
||||||
|
WinnerPhase = NestPhase.Extents;
|
||||||
|
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,373 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class FillExtents
|
||||||
|
{
|
||||||
|
private const int MaxIterations = 10;
|
||||||
|
|
||||||
|
private readonly Box workArea;
|
||||||
|
private readonly double partSpacing;
|
||||||
|
private readonly double halfSpacing;
|
||||||
|
|
||||||
|
public FillExtents(Box workArea, double partSpacing)
|
||||||
|
{
|
||||||
|
this.workArea = workArea;
|
||||||
|
this.partSpacing = partSpacing;
|
||||||
|
halfSpacing = partSpacing / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
|
||||||
|
int plateNumber = 0,
|
||||||
|
CancellationToken token = default,
|
||||||
|
IProgress<NestProgress> progress = null)
|
||||||
|
{
|
||||||
|
var pair = BuildPair(drawing, rotationAngle);
|
||||||
|
if (pair == null)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox);
|
||||||
|
if (column.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
|
||||||
|
column, workArea, $"Extents: initial column {column.Count} parts");
|
||||||
|
|
||||||
|
var adjusted = AdjustColumn(pair.Value, column, token);
|
||||||
|
|
||||||
|
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
|
||||||
|
adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts");
|
||||||
|
|
||||||
|
var result = RepeatColumns(adjusted, token);
|
||||||
|
|
||||||
|
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
|
||||||
|
result, workArea, $"Extents: {result.Count} parts total");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Step 1: Pair Construction ---
|
||||||
|
|
||||||
|
private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle)
|
||||||
|
{
|
||||||
|
var part1 = Part.CreateAtOrigin(drawing, rotationAngle);
|
||||||
|
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)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Slide part2 toward part1 from the right using geometry-aware distance.
|
||||||
|
var boundary1 = new PartBoundary(part1, halfSpacing);
|
||||||
|
var boundary2 = new PartBoundary(part2, halfSpacing);
|
||||||
|
|
||||||
|
// Position part2 to the right of part1 at bounding box width distance.
|
||||||
|
var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing;
|
||||||
|
part2.Offset(startOffset, 0);
|
||||||
|
part2.UpdateBounds();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (dist < double.MaxValue && dist > 0)
|
||||||
|
{
|
||||||
|
part2.Offset(-dist, 0);
|
||||||
|
part2.UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-anchor pair to work area origin.
|
||||||
|
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||||
|
var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom);
|
||||||
|
part1.Offset(anchor);
|
||||||
|
part2.Offset(anchor);
|
||||||
|
part1.UpdateBounds();
|
||||||
|
part2.UpdateBounds();
|
||||||
|
|
||||||
|
pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||||
|
|
||||||
|
// Verify pair fits in work area.
|
||||||
|
if (pairBbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||||
|
pairBbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (part1, part2, pairBbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Step 2: Build Column (tile vertically) ---
|
||||||
|
|
||||||
|
private List<Part> BuildColumn(Part part1, Part part2, Box pairBbox)
|
||||||
|
{
|
||||||
|
var column = new List<Part> { (Part)part1.Clone(), (Part)part2.Clone() };
|
||||||
|
|
||||||
|
// Find geometry-aware copy distance for the pair vertically.
|
||||||
|
var boundary1 = new PartBoundary(part1, halfSpacing);
|
||||||
|
var boundary2 = new PartBoundary(part2, halfSpacing);
|
||||||
|
|
||||||
|
// Compute vertical copy distance using bounding boxes as starting point,
|
||||||
|
// then slide down to find true geometry distance.
|
||||||
|
var pairHeight = pairBbox.Length;
|
||||||
|
var testOffset = new Vector(0, pairHeight);
|
||||||
|
|
||||||
|
// Create test parts for slide distance measurement.
|
||||||
|
var testPart1 = part1.CloneAtOffset(testOffset);
|
||||||
|
var testPart2 = part2.CloneAtOffset(testOffset);
|
||||||
|
|
||||||
|
// Find minimum distance from test pair sliding down toward original pair.
|
||||||
|
var copyDistance = FindVerticalCopyDistance(
|
||||||
|
part1, part2, testPart1, testPart2,
|
||||||
|
boundary1, boundary2, pairHeight);
|
||||||
|
|
||||||
|
if (copyDistance <= 0)
|
||||||
|
return column;
|
||||||
|
|
||||||
|
var count = 1;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var nextBottom = pairBbox.Bottom + copyDistance * count;
|
||||||
|
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var offset = new Vector(0, copyDistance * count);
|
||||||
|
column.Add(part1.CloneAtOffset(offset));
|
||||||
|
column.Add(part2.CloneAtOffset(offset));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double FindVerticalCopyDistance(
|
||||||
|
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 minSlide = double.MaxValue;
|
||||||
|
|
||||||
|
// Test1 -> Orig1
|
||||||
|
var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down);
|
||||||
|
if (d < minSlide) minSlide = d;
|
||||||
|
|
||||||
|
// Test1 -> Orig2
|
||||||
|
d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down);
|
||||||
|
if (d < minSlide) minSlide = d;
|
||||||
|
|
||||||
|
// Test2 -> Orig1
|
||||||
|
d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down);
|
||||||
|
if (d < minSlide) minSlide = d;
|
||||||
|
|
||||||
|
// Test2 -> Orig2
|
||||||
|
d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down);
|
||||||
|
if (d < minSlide) minSlide = d;
|
||||||
|
|
||||||
|
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.
|
||||||
|
var copyDist = pairHeight - minSlide;
|
||||||
|
|
||||||
|
// Clamp: never let geometry quirks produce a distance smaller than
|
||||||
|
// the bounding box height (which would overlap).
|
||||||
|
return System.Math.Max(copyDist, pairHeight + partSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double SlideDistance(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Step 3: Iterative Adjustment ---
|
||||||
|
|
||||||
|
private List<Part> AdjustColumn(
|
||||||
|
(Part part1, Part part2, Box pairBbox) pair,
|
||||||
|
List<Part> column,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
var originalPairWidth = pair.pairBbox.Width;
|
||||||
|
|
||||||
|
for (var iteration = 0; iteration < MaxIterations; iteration++)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Measure current gap.
|
||||||
|
var topEdge = double.MinValue;
|
||||||
|
foreach (var p in column)
|
||||||
|
if (p.BoundingBox.Top > topEdge)
|
||||||
|
topEdge = p.BoundingBox.Top;
|
||||||
|
|
||||||
|
var gap = workArea.Top - topEdge;
|
||||||
|
|
||||||
|
if (gap <= Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var pairCount = column.Count / 2;
|
||||||
|
if (pairCount <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var adjustment = gap / pairCount;
|
||||||
|
if (adjustment <= Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Try adjusting the pair and rebuilding the column.
|
||||||
|
var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth);
|
||||||
|
if (adjusted == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox);
|
||||||
|
if (newColumn.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
column = newColumn;
|
||||||
|
pair = adjusted.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Part part1, Part part2, Box pairBbox)? TryAdjustPair(
|
||||||
|
(Part part1, Part part2, Box pairBbox) pair,
|
||||||
|
double adjustment, double originalPairWidth)
|
||||||
|
{
|
||||||
|
// Try shifting part2 up first.
|
||||||
|
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// Up made the pair wider — try down instead.
|
||||||
|
return TryShiftDirection(pair, -adjustment, originalPairWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Part part1, Part part2, Box pairBbox)? TryShiftDirection(
|
||||||
|
(Part part1, Part part2, Box pairBbox) pair,
|
||||||
|
double verticalShift, double originalPairWidth)
|
||||||
|
{
|
||||||
|
// Clone parts so we don't mutate the originals.
|
||||||
|
var p1 = (Part)pair.part1.Clone();
|
||||||
|
var p2 = (Part)pair.part2.Clone();
|
||||||
|
|
||||||
|
// Separate: shift part2 right so bounding boxes don't touch.
|
||||||
|
p2.Offset(partSpacing, 0);
|
||||||
|
p2.UpdateBounds();
|
||||||
|
|
||||||
|
// Apply the vertical shift.
|
||||||
|
p2.Offset(0, verticalShift);
|
||||||
|
p2.UpdateBounds();
|
||||||
|
|
||||||
|
// Compact part2 left toward part1.
|
||||||
|
var moving = new List<Part> { p2 };
|
||||||
|
var obstacles = new List<Part> { p1 };
|
||||||
|
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
|
||||||
|
|
||||||
|
// Check if the pair got wider.
|
||||||
|
var newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
|
||||||
|
|
||||||
|
if (newBbox.Width > originalPairWidth + Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Re-anchor to work area origin.
|
||||||
|
var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom);
|
||||||
|
p1.Offset(anchor);
|
||||||
|
p2.Offset(anchor);
|
||||||
|
p1.UpdateBounds();
|
||||||
|
p2.UpdateBounds();
|
||||||
|
|
||||||
|
newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
|
||||||
|
return (p1, p2, newBbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Step 4: Horizontal Repetition ---
|
||||||
|
|
||||||
|
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (column.Count == 0)
|
||||||
|
return column;
|
||||||
|
|
||||||
|
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
|
||||||
|
var columnWidth = columnBbox.Width;
|
||||||
|
|
||||||
|
// Create a test column shifted right by columnWidth + spacing.
|
||||||
|
var testOffset = columnWidth + partSpacing;
|
||||||
|
var testColumn = new List<Part>(column.Count);
|
||||||
|
foreach (var part in column)
|
||||||
|
testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0)));
|
||||||
|
|
||||||
|
// Compact the test column left against the original column.
|
||||||
|
var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
|
||||||
|
|
||||||
|
// Derive the true copy distance from where the test column ended up.
|
||||||
|
var testBbox = ((IEnumerable<IBoundable>)testColumn).GetBoundingBox();
|
||||||
|
var copyDistance = testBbox.Left - columnBbox.Left;
|
||||||
|
|
||||||
|
if (copyDistance <= Tolerance.Epsilon)
|
||||||
|
copyDistance = columnWidth + partSpacing;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
|
||||||
|
|
||||||
|
// Build all columns.
|
||||||
|
var result = new List<Part>(column);
|
||||||
|
|
||||||
|
// Add the test column we already computed as column 2.
|
||||||
|
foreach (var part in testColumn)
|
||||||
|
{
|
||||||
|
if (IsWithinWorkArea(part))
|
||||||
|
result.Add(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile additional columns at the copy distance.
|
||||||
|
var colIndex = 2;
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var offset = new Vector(copyDistance * colIndex, 0);
|
||||||
|
var anyFit = false;
|
||||||
|
|
||||||
|
foreach (var part in column)
|
||||||
|
{
|
||||||
|
var clone = part.CloneAtOffset(offset);
|
||||||
|
if (IsWithinWorkArea(clone))
|
||||||
|
{
|
||||||
|
result.Add(clone);
|
||||||
|
anyFit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyFit)
|
||||||
|
break;
|
||||||
|
|
||||||
|
colIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsWithinWorkArea(Part part)
|
||||||
|
{
|
||||||
|
return 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -307,6 +307,7 @@ namespace OpenNest
|
|||||||
case NestPhase.Pairs: return "Pairs";
|
case NestPhase.Pairs: return "Pairs";
|
||||||
case NestPhase.Linear: return "Linear";
|
case NestPhase.Linear: return "Linear";
|
||||||
case NestPhase.RectBestFit: return "BestFit";
|
case NestPhase.RectBestFit: return "BestFit";
|
||||||
|
case NestPhase.Extents: return "Extents";
|
||||||
default: return phase.ToString();
|
default: return phase.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ namespace OpenNest
|
|||||||
Linear,
|
Linear,
|
||||||
RectBestFit,
|
RectBestFit,
|
||||||
Pairs,
|
Pairs,
|
||||||
Nfp
|
Nfp,
|
||||||
|
Extents
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PhaseResult
|
public class PhaseResult
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ namespace OpenNest
|
|||||||
IProgress<NestProgress> progress = null)
|
IProgress<NestProgress> progress = null)
|
||||||
{
|
{
|
||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
item.Drawing, plateSize.Width, plateSize.Length, partSpacing);
|
item.Drawing, plateSize.Length, plateSize.Width, partSpacing);
|
||||||
|
|
||||||
var candidates = SelectPairCandidates(bestFits, workArea);
|
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||||
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
|
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
|
||||||
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Width:F2}x{plateSize.Length:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
||||||
|
|
||||||
List<Part> best = null;
|
List<Part> best = null;
|
||||||
var bestScore = default(FillScore);
|
var bestScore = default(FillScore);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace OpenNest.Engine.Sequencing
|
|||||||
rows.Sort((a, b) => a.RowY.CompareTo(b.RowY));
|
rows.Sort((a, b) => a.RowY.CompareTo(b.RowY));
|
||||||
|
|
||||||
// Determine initial direction based on exit point
|
// Determine initial direction based on exit point
|
||||||
var leftToRight = exit.X > plate.Size.Width * 0.5;
|
var leftToRight = exit.X > plate.Size.Length * 0.5;
|
||||||
|
|
||||||
var result = new List<SequencedPart>(parts.Count);
|
var result = new List<SequencedPart>(parts.Count);
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ namespace OpenNest.Engine.Sequencing
|
|||||||
{
|
{
|
||||||
public static Vector GetExitPoint(Plate plate)
|
public static Vector GetExitPoint(Plate plate)
|
||||||
{
|
{
|
||||||
var w = plate.Size.Width;
|
var xExtent = plate.Size.Length;
|
||||||
var l = plate.Size.Length;
|
var yExtent = plate.Size.Width;
|
||||||
|
|
||||||
return plate.Quadrant switch
|
return plate.Quadrant switch
|
||||||
{
|
{
|
||||||
1 => new Vector(w, l),
|
1 => new Vector(xExtent, yExtent),
|
||||||
2 => new Vector(0, l),
|
2 => new Vector(0, yExtent),
|
||||||
3 => new Vector(0, 0),
|
3 => new Vector(0, 0),
|
||||||
4 => new Vector(w, 0),
|
4 => new Vector(xExtent, 0),
|
||||||
_ => new Vector(w, l)
|
_ => new Vector(xExtent, yExtent)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -145,30 +145,30 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, plate.Size.Length, 0);
|
pt2 = new XYZ(0, plate.Size.Width, 0);
|
||||||
pt3 = new XYZ(plate.Size.Width, plate.Size.Length, 0);
|
pt3 = new XYZ(plate.Size.Length, plate.Size.Width, 0);
|
||||||
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
pt4 = new XYZ(plate.Size.Length, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, plate.Size.Length, 0);
|
pt2 = new XYZ(0, plate.Size.Width, 0);
|
||||||
pt3 = new XYZ(-plate.Size.Width, plate.Size.Length, 0);
|
pt3 = new XYZ(-plate.Size.Length, plate.Size.Width, 0);
|
||||||
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
pt4 = new XYZ(-plate.Size.Length, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, -plate.Size.Length, 0);
|
pt2 = new XYZ(0, -plate.Size.Width, 0);
|
||||||
pt3 = new XYZ(-plate.Size.Width, -plate.Size.Length, 0);
|
pt3 = new XYZ(-plate.Size.Length, -plate.Size.Width, 0);
|
||||||
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
pt4 = new XYZ(-plate.Size.Length, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, -plate.Size.Length, 0);
|
pt2 = new XYZ(0, -plate.Size.Width, 0);
|
||||||
pt3 = new XYZ(plate.Size.Width, -plate.Size.Length, 0);
|
pt3 = new XYZ(plate.Size.Length, -plate.Size.Width, 0);
|
||||||
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
pt4 = new XYZ(plate.Size.Length, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class FillExtentsTests
|
||||||
|
{
|
||||||
|
private static Drawing MakeRightTriangle(double w, double h)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("triangle", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeRect(double w, double h)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("rect", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_Triangle_ReturnsPartsWithinWorkArea()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRightTriangle(10, 8);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.True(parts.Count > 0, "Should place at least one part");
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01,
|
||||||
|
$"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}");
|
||||||
|
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01,
|
||||||
|
$"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_PartTooLarge_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 5, 5);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRect(10, 10);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.Empty(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_Triangle_ColumnFillsHeight()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRightTriangle(10, 8);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
Assert.True(parts.Count > 0);
|
||||||
|
|
||||||
|
// The topmost part should be close to the work area top edge.
|
||||||
|
var topEdge = 0.0;
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
if (part.BoundingBox.Top > topEdge)
|
||||||
|
topEdge = part.BoundingBox.Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After adjustment, the gap should be small (within one part spacing).
|
||||||
|
var gap = workArea.Top - topEdge;
|
||||||
|
Assert.True(gap < 1.0,
|
||||||
|
$"Gap of {gap:F2} is too large — adjustment should fill close to the top");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_Triangle_FillsWidthWithMultipleColumns()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRightTriangle(10, 8);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
// With a 120-wide sheet and ~10-wide parts, we should get multiple columns.
|
||||||
|
Assert.True(parts.Count >= 8,
|
||||||
|
$"Expected multiple columns but got only {parts.Count} parts");
|
||||||
|
|
||||||
|
// Verify all parts are within bounds.
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01);
|
||||||
|
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01);
|
||||||
|
Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01);
|
||||||
|
Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_Rect_ReturnsNonEmpty()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRect(15, 10);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.True(parts.Count > 0, "Rectangle should produce results");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_NonZeroOriginWorkArea_PartsWithinBounds()
|
||||||
|
{
|
||||||
|
// Simulate a remnant sub-region with non-zero origin.
|
||||||
|
var workArea = new Box(30, 10, 80, 40);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRightTriangle(10, 8);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing);
|
||||||
|
|
||||||
|
Assert.True(parts.Count > 0);
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01,
|
||||||
|
$"Part left {part.BoundingBox.Left} below work area left {workArea.Left}");
|
||||||
|
Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01,
|
||||||
|
$"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}");
|
||||||
|
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01);
|
||||||
|
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_RespectsCancellation()
|
||||||
|
{
|
||||||
|
var cts = new System.Threading.CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
var filler = new FillExtents(workArea, 0.5);
|
||||||
|
var drawing = MakeRightTriangle(10, 8);
|
||||||
|
|
||||||
|
var parts = filler.Fill(drawing, token: cts.Token);
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,7 +205,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public virtual void ZoomToArea(Box box, bool redraw = true)
|
public virtual void ZoomToArea(Box box, bool redraw = true)
|
||||||
{
|
{
|
||||||
ZoomToArea(box.X, box.Y, box.Length, box.Width, redraw);
|
ZoomToArea(box.X, box.Y, box.Width, box.Length, redraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)
|
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)
|
||||||
|
|||||||
@@ -435,24 +435,24 @@ namespace OpenNest.Controls
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, 0);
|
plateRect.Location = PointWorldToGraph(-Plate.Size.Length, 0);
|
||||||
edgeSpacingRect.Location = PointWorldToGraph(
|
edgeSpacingRect.Location = PointWorldToGraph(
|
||||||
Plate.EdgeSpacing.Left - Plate.Size.Width,
|
Plate.EdgeSpacing.Left - Plate.Size.Length,
|
||||||
Plate.EdgeSpacing.Bottom);
|
Plate.EdgeSpacing.Bottom);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Length);
|
plateRect.Location = PointWorldToGraph(-Plate.Size.Length, -Plate.Size.Width);
|
||||||
edgeSpacingRect.Location = PointWorldToGraph(
|
edgeSpacingRect.Location = PointWorldToGraph(
|
||||||
Plate.EdgeSpacing.Left - Plate.Size.Width,
|
Plate.EdgeSpacing.Left - Plate.Size.Length,
|
||||||
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
|
Plate.EdgeSpacing.Bottom - Plate.Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Length);
|
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Width);
|
||||||
edgeSpacingRect.Location = PointWorldToGraph(
|
edgeSpacingRect.Location = PointWorldToGraph(
|
||||||
Plate.EdgeSpacing.Left,
|
Plate.EdgeSpacing.Left,
|
||||||
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
|
Plate.EdgeSpacing.Bottom - Plate.Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ namespace OpenNest.Forms
|
|||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
var all = BestFitCache.GetOrCompute(
|
var all = BestFitCache.GetOrCompute(
|
||||||
drawing, plate.Size.Width, plate.Size.Length, plate.PartSpacing);
|
drawing, plate.Size.Length, plate.Size.Width, plate.PartSpacing);
|
||||||
|
|
||||||
computeSeconds = sw.ElapsedMilliseconds / 1000.0;
|
computeSeconds = sw.ElapsedMilliseconds / 1000.0;
|
||||||
totalResults = all.Count;
|
totalResults = all.Count;
|
||||||
|
|||||||
Reference in New Issue
Block a user