diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index c3c4ba0..87c257a 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -119,6 +119,9 @@ namespace OpenNest 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) { Rotate(oneAndHalfPI); @@ -128,19 +131,19 @@ namespace OpenNest switch (Quadrant) { case 1: - Offset(0, Size.Length); + Offset(0, Size.Width); break; case 2: - Offset(-Size.Width, 0); + Offset(-Size.Length, 0); break; case 3: - Offset(0, -Size.Length); + Offset(0, -Size.Width); break; case 4: - Offset(Size.Width, 0); + Offset(Size.Length, 0); break; default: @@ -161,19 +164,19 @@ namespace OpenNest switch (Quadrant) { case 1: - Offset(Size.Width, 0); + Offset(Size.Length, 0); break; case 2: - Offset(0, Size.Length); + Offset(0, Size.Width); break; case 3: - Offset(-Size.Width, 0); + Offset(-Size.Length, 0); break; case 4: - Offset(0, -Size.Length); + Offset(0, -Size.Width); break; default: @@ -200,19 +203,19 @@ namespace OpenNest switch (Quadrant) { 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; 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; 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; 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; default: @@ -294,6 +297,7 @@ namespace OpenNest { var plateBox = new Box(); + // Convention: Size.Length = X axis (horizontal), Size.Width = Y axis (vertical) switch (Quadrant) { case 1: @@ -302,26 +306,26 @@ namespace OpenNest break; case 2: - plateBox.X = (float)-Size.Width; + plateBox.X = (float)-Size.Length; plateBox.Y = 0; break; case 3: - plateBox.X = (float)-Size.Width; - plateBox.Y = (float)-Size.Length; + plateBox.X = (float)-Size.Length; + plateBox.Y = (float)-Size.Width; break; case 4: plateBox.X = 0; - plateBox.Y = (float)-Size.Length; + plateBox.Y = (float)-Size.Width; break; default: return new Box(); } - plateBox.Width = Size.Width; - plateBox.Length = Size.Length; + plateBox.Width = Size.Length; + plateBox.Length = Size.Width; if (!includeParts) return plateBox; @@ -382,29 +386,30 @@ namespace OpenNest var bounds = Parts.GetBoundingBox(); - double width; - double length; + // Convention: Length = X axis, Width = Y axis + double xExtent; + double yExtent; switch (Quadrant) { case 1: - width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; - length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; + xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; + yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 2: - width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; - length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; + xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; + yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 3: - width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; - length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; + xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; + yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; case 4: - width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; - length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; + xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; + yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; default: @@ -412,8 +417,8 @@ namespace OpenNest } Size = new Size( - Rounding.RoundUpToNearest(width, roundingFactor), - Rounding.RoundUpToNearest(length, roundingFactor)); + Rounding.RoundUpToNearest(yExtent, roundingFactor), + Rounding.RoundUpToNearest(xExtent, roundingFactor)); } /// diff --git a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs index f498ae3..8db666e 100644 --- a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs +++ b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs @@ -8,8 +8,8 @@ namespace OpenNest.Engine.BestFit.Tiling { public TileResult Evaluate(BestFitResult bestFit, Plate plate) { - var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right; - var plateHeight = plate.Size.Length - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom; + var plateWidth = plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right; + var plateHeight = plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom; var result1 = TryTile(bestFit, plateWidth, plateHeight, false); var result2 = TryTile(bestFit, plateWidth, plateHeight, true); diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index 8cb765e..118471d 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -17,7 +17,7 @@ namespace OpenNest 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(); @@ -71,7 +71,7 @@ namespace OpenNest // Top pair candidates — check if pairs tile better in this box. 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); foreach (var pair in topPairs) @@ -146,6 +146,29 @@ namespace OpenNest best = pairResult; 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 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) { @@ -263,6 +286,34 @@ namespace OpenNest WinnerPhase = NestPhase.RectBestFit; ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary()); } + + // Extents phase + token.ThrowIfCancellationRequested(); + var extentsSw = Stopwatch.StartNew(); + var extentsFiller = new FillExtents(workArea, Plate.PartSpacing); + List 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) { diff --git a/OpenNest.Engine/FillExtents.cs b/OpenNest.Engine/FillExtents.cs new file mode 100644 index 0000000..bbcbf47 --- /dev/null +++ b/OpenNest.Engine/FillExtents.cs @@ -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 Fill(Drawing drawing, double rotationAngle = 0, + int plateNumber = 0, + CancellationToken token = default, + IProgress progress = null) + { + var pair = BuildPair(drawing, rotationAngle); + if (pair == null) + return new List(); + + var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox); + if (column.Count == 0) + return new List(); + + 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)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)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 BuildColumn(Part part1, Part part2, Box pairBbox) + { + var column = new List { (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 AdjustColumn( + (Part part1, Part part2, Box pairBbox) pair, + List 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 { p2 }; + var obstacles = new List { p1 }; + Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left); + + // Check if the pair got wider. + var newBbox = ((IEnumerable)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)new IBoundable[] { p1, p2 }).GetBoundingBox(); + return (p1, p2, newBbox); + } + + // --- Step 4: Horizontal Repetition --- + + private List RepeatColumns(List column, CancellationToken token) + { + if (column.Count == 0) + return column; + + var columnBbox = ((IEnumerable)column).GetBoundingBox(); + var columnWidth = columnBbox.Width; + + // Create a test column shifted right by columnWidth + spacing. + var testOffset = columnWidth + partSpacing; + var testColumn = new List(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)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(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; + } + } +} diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 681eacb..afb6123 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -307,6 +307,7 @@ namespace OpenNest case NestPhase.Pairs: return "Pairs"; case NestPhase.Linear: return "Linear"; case NestPhase.RectBestFit: return "BestFit"; + case NestPhase.Extents: return "Extents"; default: return phase.ToString(); } } diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 410fe50..bf76ec9 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -8,7 +8,8 @@ namespace OpenNest Linear, RectBestFit, Pairs, - Nfp + Nfp, + Extents } public class PhaseResult diff --git a/OpenNest.Engine/PairFiller.cs b/OpenNest.Engine/PairFiller.cs index b448c96..8d69362 100644 --- a/OpenNest.Engine/PairFiller.cs +++ b/OpenNest.Engine/PairFiller.cs @@ -30,11 +30,11 @@ namespace OpenNest IProgress progress = null) { var bestFits = BestFitCache.GetOrCompute( - item.Drawing, plateSize.Width, plateSize.Length, partSpacing); + item.Drawing, plateSize.Length, plateSize.Width, partSpacing); var candidates = SelectPairCandidates(bestFits, workArea); Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); - Debug.WriteLine($"[PairFiller] Plate: {plateSize.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 best = null; var bestScore = default(FillScore); diff --git a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs index fa285aa..106b60c 100644 --- a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs +++ b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs @@ -28,7 +28,7 @@ namespace OpenNest.Engine.Sequencing rows.Sort((a, b) => a.RowY.CompareTo(b.RowY)); // 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(parts.Count); foreach (var row in rows) diff --git a/OpenNest.Engine/Sequencing/PlateHelper.cs b/OpenNest.Engine/Sequencing/PlateHelper.cs index 1a46327..04e3282 100644 --- a/OpenNest.Engine/Sequencing/PlateHelper.cs +++ b/OpenNest.Engine/Sequencing/PlateHelper.cs @@ -6,16 +6,16 @@ namespace OpenNest.Engine.Sequencing { public static Vector GetExitPoint(Plate plate) { - var w = plate.Size.Width; - var l = plate.Size.Length; + var xExtent = plate.Size.Length; + var yExtent = plate.Size.Width; return plate.Quadrant switch { - 1 => new Vector(w, l), - 2 => new Vector(0, l), + 1 => new Vector(xExtent, yExtent), + 2 => new Vector(0, yExtent), 3 => new Vector(0, 0), - 4 => new Vector(w, 0), - _ => new Vector(w, l) + 4 => new Vector(xExtent, 0), + _ => new Vector(xExtent, yExtent) }; } } diff --git a/OpenNest.IO/DxfExporter.cs b/OpenNest.IO/DxfExporter.cs index aac67a3..2cfa222 100644 --- a/OpenNest.IO/DxfExporter.cs +++ b/OpenNest.IO/DxfExporter.cs @@ -145,30 +145,30 @@ namespace OpenNest.IO { case 1: pt1 = new XYZ(0, 0, 0); - pt2 = new XYZ(0, plate.Size.Length, 0); - pt3 = new XYZ(plate.Size.Width, plate.Size.Length, 0); - pt4 = new XYZ(plate.Size.Width, 0, 0); + pt2 = new XYZ(0, plate.Size.Width, 0); + pt3 = new XYZ(plate.Size.Length, plate.Size.Width, 0); + pt4 = new XYZ(plate.Size.Length, 0, 0); break; case 2: pt1 = new XYZ(0, 0, 0); - pt2 = new XYZ(0, plate.Size.Length, 0); - pt3 = new XYZ(-plate.Size.Width, plate.Size.Length, 0); - pt4 = new XYZ(-plate.Size.Width, 0, 0); + pt2 = new XYZ(0, plate.Size.Width, 0); + pt3 = new XYZ(-plate.Size.Length, plate.Size.Width, 0); + pt4 = new XYZ(-plate.Size.Length, 0, 0); break; case 3: pt1 = new XYZ(0, 0, 0); - pt2 = new XYZ(0, -plate.Size.Length, 0); - pt3 = new XYZ(-plate.Size.Width, -plate.Size.Length, 0); - pt4 = new XYZ(-plate.Size.Width, 0, 0); + pt2 = new XYZ(0, -plate.Size.Width, 0); + pt3 = new XYZ(-plate.Size.Length, -plate.Size.Width, 0); + pt4 = new XYZ(-plate.Size.Length, 0, 0); break; case 4: pt1 = new XYZ(0, 0, 0); - pt2 = new XYZ(0, -plate.Size.Length, 0); - pt3 = new XYZ(plate.Size.Width, -plate.Size.Length, 0); - pt4 = new XYZ(plate.Size.Width, 0, 0); + pt2 = new XYZ(0, -plate.Size.Width, 0); + pt3 = new XYZ(plate.Size.Length, -plate.Size.Width, 0); + pt4 = new XYZ(plate.Size.Length, 0, 0); break; default: diff --git a/OpenNest.Tests/FillExtentsTests.cs b/OpenNest.Tests/FillExtentsTests.cs new file mode 100644 index 0000000..3271ec6 --- /dev/null +++ b/OpenNest.Tests/FillExtentsTests.cs @@ -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); + } +} diff --git a/OpenNest/Controls/DrawControl.cs b/OpenNest/Controls/DrawControl.cs index dbaf2ca..3d9f2fc 100644 --- a/OpenNest/Controls/DrawControl.cs +++ b/OpenNest/Controls/DrawControl.cs @@ -205,7 +205,7 @@ namespace OpenNest.Controls 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) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 630a950..c61d6b4 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -435,24 +435,24 @@ namespace OpenNest.Controls break; case 2: - plateRect.Location = PointWorldToGraph(-Plate.Size.Width, 0); + plateRect.Location = PointWorldToGraph(-Plate.Size.Length, 0); edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left - Plate.Size.Width, + Plate.EdgeSpacing.Left - Plate.Size.Length, Plate.EdgeSpacing.Bottom); break; case 3: - plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Length); + plateRect.Location = PointWorldToGraph(-Plate.Size.Length, -Plate.Size.Width); edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left - Plate.Size.Width, - Plate.EdgeSpacing.Bottom - Plate.Size.Length); + Plate.EdgeSpacing.Left - Plate.Size.Length, + Plate.EdgeSpacing.Bottom - Plate.Size.Width); break; case 4: - plateRect.Location = PointWorldToGraph(0, -Plate.Size.Length); + plateRect.Location = PointWorldToGraph(0, -Plate.Size.Width); edgeSpacingRect.Location = PointWorldToGraph( Plate.EdgeSpacing.Left, - Plate.EdgeSpacing.Bottom - Plate.Size.Length); + Plate.EdgeSpacing.Bottom - Plate.Size.Width); break; default: diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index 5d590a7..e168c50 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -77,7 +77,7 @@ namespace OpenNest.Forms var sw = Stopwatch.StartNew(); 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; totalResults = all.Count;