diff --git a/OpenNest.Core/Geometry/Polygon.cs b/OpenNest.Core/Geometry/Polygon.cs index c6b8d4b..8aa8ac2 100644 --- a/OpenNest.Core/Geometry/Polygon.cs +++ b/OpenNest.Core/Geometry/Polygon.cs @@ -608,5 +608,29 @@ namespace OpenNest.Geometry var hull = ConvexHull.Compute(Vertices); return RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle); } + + public bool ContainsPoint(Vector pt) + { + var n = IsClosed() ? Vertices.Count - 1 : Vertices.Count; + + if (n < 3) + return false; + + var inside = false; + + for (int i = 0, j = n - 1; i < n; j = i++) + { + var vi = Vertices[i]; + var vj = Vertices[j]; + + if ((vi.Y > pt.Y) != (vj.Y > pt.Y) && + pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X) + { + inside = !inside; + } + } + + return inside; + } } } diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs index ac1e523..e88a60c 100644 --- a/OpenNest.Core/Part.cs +++ b/OpenNest.Core/Part.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; @@ -130,8 +131,10 @@ namespace OpenNest { pts = new List(); - var entities1 = ConvertProgram.ToGeometry(Program); - var entities2 = ConvertProgram.ToGeometry(part.Program); + var entities1 = ConvertProgram.ToGeometry(Program) + .Where(e => e.Layer != SpecialLayers.Rapid); + var entities2 = ConvertProgram.ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid); var shapes1 = Helper.GetShapes(entities1); var shapes2 = Helper.GetShapes(entities2); diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index 9aede62..074c31d 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -1,5 +1,7 @@ +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; @@ -9,12 +11,12 @@ namespace OpenNest.Engine.BestFit { public class BestFitFinder { - private readonly PairEvaluator _evaluator; + private readonly IPairEvaluator _evaluator; private readonly BestFitFilter _filter; - public BestFitFinder(double maxPlateWidth, double maxPlateHeight) + public BestFitFinder(double maxPlateWidth, double maxPlateHeight, IPairEvaluator evaluator = null) { - _evaluator = new PairEvaluator(); + _evaluator = evaluator ?? new PairEvaluator(); _filter = new BestFitFilter { MaxPlateWidth = maxPlateWidth, @@ -30,12 +32,16 @@ namespace OpenNest.Engine.BestFit { var strategies = BuildStrategies(drawing); - var allCandidates = new List(); + var candidateBags = new ConcurrentBag>(); - foreach (var strategy in strategies) - allCandidates.AddRange(strategy.GenerateCandidates(drawing, spacing, stepSize)); + Parallel.ForEach(strategies, strategy => + { + candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize)); + }); - var results = allCandidates.Select(c => _evaluator.Evaluate(c)).ToList(); + var allCandidates = candidateBags.SelectMany(c => c).ToList(); + + var results = _evaluator.EvaluateAll(allCandidates); _filter.Apply(results); diff --git a/OpenNest.Engine/BestFit/IPairEvaluator.cs b/OpenNest.Engine/BestFit/IPairEvaluator.cs new file mode 100644 index 0000000..159d3c2 --- /dev/null +++ b/OpenNest.Engine/BestFit/IPairEvaluator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace OpenNest.Engine.BestFit +{ + public interface IPairEvaluator + { + List EvaluateAll(List candidates); + } +} diff --git a/OpenNest.Engine/BestFit/PairEvaluator.cs b/OpenNest.Engine/BestFit/PairEvaluator.cs index 814a88d..c82df0c 100644 --- a/OpenNest.Engine/BestFit/PairEvaluator.cs +++ b/OpenNest.Engine/BestFit/PairEvaluator.cs @@ -1,15 +1,29 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.BestFit { - public class PairEvaluator + public class PairEvaluator : IPairEvaluator { private const double ChordTolerance = 0.01; + public List EvaluateAll(List candidates) + { + var resultBag = new ConcurrentBag(); + + Parallel.ForEach(candidates, c => + { + resultBag.Add(Evaluate(c)); + }); + + return resultBag.ToList(); + } + public BestFitResult Evaluate(PairCandidate candidate) { var drawing = candidate.Drawing; diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 2447213..8cb6162 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -80,7 +80,12 @@ namespace OpenNest.Engine.BestFit // Pre-compute part1's offset lines (half-spacing outward) var part1Lines = Helper.GetOffsetPartLines(part1, halfSpacing); - for (var offset = perpMin; offset <= perpMax; offset += stepSize) + // Align sweep start to a multiple of stepSize so that offset=0 is always + // included. This ensures perfect grid arrangements (side-by-side, stacked) + // are generated for rectangular parts. + var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize; + + for (var offset = alignedStart; offset <= perpMax; offset += stepSize) { var part2 = (Part)part2Template.Clone(); diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 97a9c18..db809e7 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using OpenNest.Converters; using OpenNest.Engine.BestFit; @@ -21,8 +22,12 @@ namespace OpenNest public NestDirection NestDirection { get; set; } + public Func CreateEvaluator { get; set; } + public bool Fill(NestItem item) { + var sw = Stopwatch.StartNew(); + var workArea = Plate.WorkArea(); var bestRotation = FindBestRotation(item); @@ -37,22 +42,37 @@ namespace OpenNest engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical) }; - // Pick the linear configuration with the most parts. + // Pick the best linear configuration. FillLinear already ensures + // geometry-aware spacing, so skip the redundant overlap check that + // can produce false positives on arcs/curves. List linearBest = null; foreach (var config in configs) { - if (linearBest == null || config.Count > linearBest.Count) + if (IsBetterFill(config, linearBest)) linearBest = config; } + var linearMs = sw.ElapsedMilliseconds; + + // Try rectangle best-fit (mixes orientations to fill remnant strips). + var rectResult = FillRectangleBestFit(item, workArea); + + var rectMs = sw.ElapsedMilliseconds - linearMs; + // Try pair-based approach. var pairResult = FillWithPairs(item); - // Pick whichever produced more parts. + var pairMs = sw.ElapsedMilliseconds - linearMs - rectMs; + + // Pick whichever is the better fill. + Debug.WriteLine($"[NestEngine.Fill] Linear: {linearBest?.Count ?? 0} parts ({linearMs}ms) | Rect: {rectResult?.Count ?? 0} parts ({rectMs}ms) | Pair: {pairResult.Count} parts ({pairMs}ms) | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); var best = linearBest; - if (pairResult.Count > (best?.Count ?? 0)) + if (IsBetterFill(rectResult, best)) + best = rectResult; + + if (IsBetterFill(pairResult, best)) best = pairResult; if (best == null || best.Count == 0) @@ -76,6 +96,21 @@ namespace OpenNest var angles = FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles); + // For single-part groups, also try rectangle best-fit and pair-based filling. + if (groupParts.Count == 1) + { + var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing }; + var rectResult = FillRectangleBestFit(nestItem, workArea); + + if (IsBetterFill(rectResult, best)) + best = rectResult; + + var pairResult = FillWithPairs(nestItem); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + } + if (best == null || best.Count == 0) return false; @@ -101,10 +136,28 @@ namespace OpenNest foreach (var config in configs) { - if (best == null || config.Count > best.Count) + if (IsBetterFill(config, best)) best = config; } + Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); + + // Try rectangle best-fit (mixes orientations to fill remnant strips). + var rectResult = FillRectangleBestFit(item, workArea); + + Debug.WriteLine($"[Fill(NestItem,Box)] RectBestFit: {rectResult?.Count ?? 0} parts"); + + if (IsBetterFill(rectResult, best)) + best = rectResult; + + // Try pair-based approach. + var pairResult = FillWithPairs(item, workArea); + + Debug.WriteLine($"[Fill(NestItem,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + if (best == null || best.Count == 0) return false; @@ -124,6 +177,26 @@ namespace OpenNest var angles = FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles); + Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); + + if (groupParts.Count == 1) + { + var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing }; + var rectResult = FillRectangleBestFit(nestItem, workArea); + + Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts"); + + if (IsBetterFill(rectResult, best)) + best = rectResult; + + var pairResult = FillWithPairs(nestItem, workArea); + + Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + } + if (best == null || best.Count == 0) return false; @@ -221,16 +294,106 @@ namespace OpenNest return parts.Count > 0; } + private List FillRectangleBestFit(NestItem item, Box workArea) + { + var binItem = ConvertToRectangleItem(item); + + var bin = new Bin + { + Location = workArea.Location, + Size = workArea.Size + }; + + bin.Width += Plate.PartSpacing; + bin.Height += Plate.PartSpacing; + + var engine = new FillBestFit(bin); + engine.Fill(binItem); + + var nestItems = new List { item }; + return ConvertToParts(bin, nestItems); + } + private List FillWithPairs(NestItem item) { - var finder = new BestFitFinder(Plate.Size.Width, Plate.Size.Height); - var tileResults = finder.FindAndTile(item.Drawing, Plate, Plate.PartSpacing); + return FillWithPairs(item, Plate.WorkArea()); + } - if (tileResults.Count == 0) - return new List(); + private List FillWithPairs(NestItem item, Box workArea) + { + IPairEvaluator evaluator = null; - var bestTile = tileResults[0]; - return ConvertTileResultToParts(bestTile, item.Drawing); + if (CreateEvaluator != null) + { + try { evaluator = CreateEvaluator(item.Drawing, Plate.PartSpacing); } + catch { /* GPU not available, fall back to geometry */ } + } + + var finder = new BestFitFinder(Plate.Size.Width, Plate.Size.Height, evaluator); + var bestFits = finder.FindBestFits(item.Drawing, Plate.PartSpacing, stepSize: 0.25); + + var keptResults = bestFits.Where(r => r.Keep).Take(50).ToList(); + Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {keptResults.Count}"); + + var resultBag = new System.Collections.Concurrent.ConcurrentBag<(int count, List parts)>(); + + System.Threading.Tasks.Parallel.For(0, keptResults.Count, i => + { + var result = keptResults[i]; + var pairParts = BuildPairParts(result, item.Drawing); + var angles = FindHullEdgeAngles(pairParts); + var engine = new FillLinear(workArea, Plate.PartSpacing); + var filled = FillPattern(engine, pairParts, angles); + + if (filled != null && filled.Count > 0) + resultBag.Add((filled.Count, filled)); + }); + + List best = null; + + foreach (var (count, parts) in resultBag) + { + if (best == null || count > best.Count) + best = parts; + } + + (evaluator as IDisposable)?.Dispose(); + + Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts"); + return best ?? new List(); + } + + public static List BuildPairParts(BestFitResult bestFit, Drawing drawing) + { + var candidate = bestFit.Candidate; + + var part1 = new Part(drawing); + var bbox1 = part1.Program.BoundingBox(); + part1.Offset(-bbox1.Location.X, -bbox1.Location.Y); + part1.UpdateBounds(); + + var part2 = new Part(drawing); + if (!candidate.Part2Rotation.IsEqualTo(0)) + part2.Rotate(candidate.Part2Rotation); + var bbox2 = part2.Program.BoundingBox(); + part2.Offset(-bbox2.Location.X, -bbox2.Location.Y); + part2.Location = candidate.Part2Offset; + part2.UpdateBounds(); + + if (!bestFit.OptimalRotation.IsEqualTo(0)) + { + var pairBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var center = pairBounds.Center; + part1.Rotate(-bestFit.OptimalRotation, center); + part2.Rotate(-bestFit.OptimalRotation, center); + } + + var finalBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom); + part1.Offset(offset); + part2.Offset(offset); + + return new List { part1, part2 }; } private List ConvertTileResultToParts(TileResult tileResult, Drawing drawing) @@ -295,6 +458,64 @@ namespace OpenNest return parts; } + private bool HasOverlaps(List parts, double spacing) + { + if (parts == null || parts.Count <= 1) + return false; + + for (var i = 0; i < parts.Count; i++) + { + for (var j = i + 1; j < parts.Count; j++) + { + List pts; + + if (parts[i].Intersects(parts[j], out pts)) + return true; + } + } + + return false; + } + + private bool IsBetterFill(List candidate, List current) + { + if (candidate == null || candidate.Count == 0) + return false; + + if (current == null || current.Count == 0) + return true; + + if (candidate.Count != current.Count) + return candidate.Count > current.Count; + + // Same count: prefer smaller bounding box (more compact). + var candidateBox = ((IEnumerable)candidate).GetBoundingBox(); + var currentBox = ((IEnumerable)current).GetBoundingBox(); + + return candidateBox.Area() < currentBox.Area(); + } + + private bool IsBetterValidFill(List candidate, List current) + { + if (candidate == null || candidate.Count == 0) + return false; + + // Reject candidate if it has overlapping parts. + if (HasOverlaps(candidate, Plate.PartSpacing)) + return false; + + if (current == null || current.Count == 0) + return true; + + if (candidate.Count != current.Count) + return candidate.Count > current.Count; + + var candidateBox = ((IEnumerable)candidate).GetBoundingBox(); + var currentBox = ((IEnumerable)current).GetBoundingBox(); + + return candidateBox.Area() < currentBox.Area(); + } + private List FindHullEdgeAngles(List parts) { var points = new List(); @@ -376,10 +597,10 @@ namespace OpenNest var h = engine.Fill(pattern, NestDirection.Horizontal); var v = engine.Fill(pattern, NestDirection.Vertical); - if (best == null || h.Count > best.Count) + if (IsBetterValidFill(h, best)) best = h; - if (best == null || v.Count > best.Count) + if (IsBetterValidFill(v, best)) best = v; } diff --git a/OpenNest.Gpu/GpuPairEvaluator.cs b/OpenNest.Gpu/GpuPairEvaluator.cs new file mode 100644 index 0000000..c88d00e --- /dev/null +++ b/OpenNest.Gpu/GpuPairEvaluator.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ILGPU; +using ILGPU.Runtime; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; + +namespace OpenNest.Gpu +{ + public class GpuPairEvaluator : IPairEvaluator, IDisposable + { + private readonly Context _context; + private readonly Accelerator _accelerator; + private readonly Drawing _drawing; + private readonly double _spacing; + private readonly double _cellSize; + + public GpuPairEvaluator(Drawing drawing, double spacing, double cellSize = PartBitmap.DefaultCellSize) + { + _drawing = drawing; + _spacing = spacing; + _cellSize = cellSize; + _context = Context.CreateDefault(); + _accelerator = _context.GetPreferredDevice(preferCPU: false) + .CreateAccelerator(_context); + } + + public List EvaluateAll(List candidates) + { + if (candidates.Count == 0) + return new List(); + + var dilation = _spacing / 2.0; + var bitmapA = PartBitmap.FromDrawing(_drawing, _cellSize, dilation); + + if (bitmapA.Width == 0 || bitmapA.Height == 0) + return candidates.Select(c => MakeEmptyResult(c)).ToList(); + + // Group candidates by Part2Rotation so we rasterize B once per unique rotation + var groups = candidates + .Select((c, i) => new { Candidate = c, OriginalIndex = i }) + .GroupBy(x => System.Math.Round(x.Candidate.Part2Rotation, 6)); + + var allResults = new BestFitResult[candidates.Count]; + var trueArea = _drawing.Area * 2; + + foreach (var group in groups) + { + var rotation = group.Key; + var groupItems = group.ToList(); + + // Rasterize B at this rotation + var bitmapB = PartBitmap.FromDrawingRotated(_drawing, rotation, _cellSize, dilation); + + if (bitmapB.Width == 0 || bitmapB.Height == 0) + { + foreach (var item in groupItems) + allResults[item.OriginalIndex] = MakeEmptyResult(item.Candidate); + continue; + } + + // Use the max dimensions so both bitmaps fit on the same grid + var gridWidth = System.Math.Max(bitmapA.Width, bitmapB.Width); + var gridHeight = System.Math.Max(bitmapA.Height, bitmapB.Height); + + var paddedA = PadBitmap(bitmapA, gridWidth, gridHeight); + var paddedB = PadBitmap(bitmapB, gridWidth, gridHeight); + + // Pack candidate offsets: convert world offset to cell offset + var candidateCount = groupItems.Count; + var offsets = new float[candidateCount * 3]; + + for (var i = 0; i < candidateCount; i++) + { + var c = groupItems[i].Candidate; + // Convert world-space offset to cell-space offset relative to bitmapA origin + offsets[i * 3 + 0] = (float)((c.Part2Offset.X - bitmapA.OriginX + bitmapB.OriginX) / _cellSize); + offsets[i * 3 + 1] = (float)((c.Part2Offset.Y - bitmapA.OriginY + bitmapB.OriginY) / _cellSize); + offsets[i * 3 + 2] = (float)c.Part2Rotation; + } + + var resultScores = new float[candidateCount]; + + using var gpuPaddedA = _accelerator.Allocate1D(paddedA); + using var gpuPaddedB = _accelerator.Allocate1D(paddedB); + using var gpuOffsets = _accelerator.Allocate1D(offsets); + using var gpuResults = _accelerator.Allocate1D(resultScores); + + var kernel = _accelerator.LoadAutoGroupedStreamKernel< + Index1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + int, int>(NestingKernel); + + kernel(candidateCount, gpuPaddedA.View, gpuPaddedB.View, + gpuOffsets.View, gpuResults.View, gridWidth, gridHeight); + + _accelerator.Synchronize(); + gpuResults.CopyToCPU(resultScores); + + // Map results back + for (var i = 0; i < candidateCount; i++) + { + var item = groupItems[i]; + var score = resultScores[i]; + var hasOverlap = score <= 0f; + + var combinedWidth = gridWidth * _cellSize; + var combinedHeight = gridHeight * _cellSize; + + allResults[item.OriginalIndex] = new BestFitResult + { + Candidate = item.Candidate, + RotatedArea = hasOverlap ? 0 : combinedWidth * combinedHeight, + BoundingWidth = combinedWidth, + BoundingHeight = combinedHeight, + OptimalRotation = 0, + TrueArea = trueArea, + Keep = !hasOverlap, + Reason = hasOverlap ? "Overlap detected" : "Valid" + }; + } + } + + return allResults.ToList(); + } + + private static void NestingKernel( + Index1D index, + ArrayView1D partBitmapA, + ArrayView1D partBitmapB, + ArrayView1D candidateOffsets, + ArrayView1D results, + int gridWidth, + int gridHeight) + { + var candidateIdx = index * 3; + var offsetX = candidateOffsets[candidateIdx]; + var offsetY = candidateOffsets[candidateIdx + 1]; + // rotation is already baked into partBitmapB, offset is what matters + + var overlapCount = 0; + var totalOccupied = 0; + + for (var y = 0; y < gridHeight; y++) + { + for (var x = 0; x < gridWidth; x++) + { + var cellA = partBitmapA[y * gridWidth + x]; + + // Apply offset to look up part B's cell + var bx = (int)(x - offsetX); + var by = (int)(y - offsetY); + + var cellB = 0; + if (bx >= 0 && bx < gridWidth && by >= 0 && by < gridHeight) + cellB = partBitmapB[by * gridWidth + bx]; + + if (cellA == 1 && cellB == 1) + overlapCount++; + if (cellA == 1 || cellB == 1) + totalOccupied++; + } + } + + if (overlapCount > 0) + results[index] = 0f; + else + results[index] = (float)totalOccupied / (gridWidth * gridHeight); + } + + private static int[] PadBitmap(PartBitmap bitmap, int targetWidth, int targetHeight) + { + if (bitmap.Width == targetWidth && bitmap.Height == targetHeight) + return bitmap.Cells; + + var padded = new int[targetWidth * targetHeight]; + + for (var y = 0; y < bitmap.Height; y++) + { + for (var x = 0; x < bitmap.Width; x++) + { + padded[y * targetWidth + x] = bitmap.Cells[y * bitmap.Width + x]; + } + } + + return padded; + } + + private static BestFitResult MakeEmptyResult(PairCandidate candidate) + { + return new BestFitResult + { + Candidate = candidate, + RotatedArea = 0, + BoundingWidth = 0, + BoundingHeight = 0, + OptimalRotation = 0, + TrueArea = 0, + Keep = false, + Reason = "No geometry" + }; + } + + public void Dispose() + { + _accelerator?.Dispose(); + _context?.Dispose(); + } + } +} diff --git a/OpenNest.Gpu/OpenNest.Gpu.csproj b/OpenNest.Gpu/OpenNest.Gpu.csproj new file mode 100644 index 0000000..ba271a1 --- /dev/null +++ b/OpenNest.Gpu/OpenNest.Gpu.csproj @@ -0,0 +1,15 @@ + + + net8.0-windows + OpenNest.Gpu + OpenNest.Gpu + + + + + + + + + + diff --git a/OpenNest.Gpu/PartBitmap.cs b/OpenNest.Gpu/PartBitmap.cs new file mode 100644 index 0000000..b141280 --- /dev/null +++ b/OpenNest.Gpu/PartBitmap.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Gpu +{ + public class PartBitmap + { + public int[] Cells { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public double CellSize { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + + public const double DefaultCellSize = 0.05; + + public static PartBitmap FromDrawing(Drawing drawing, double cellSize = DefaultCellSize, double spacingDilation = 0) + { + var polygons = GetClosedPolygons(drawing); + return Rasterize(polygons, cellSize, spacingDilation); + } + + public static PartBitmap FromDrawingRotated(Drawing drawing, double rotation, double cellSize = DefaultCellSize, double spacingDilation = 0) + { + var polygons = GetClosedPolygons(drawing); + + if (!rotation.IsEqualTo(0)) + { + foreach (var poly in polygons) + poly.Rotate(rotation); + } + + return Rasterize(polygons, cellSize, spacingDilation); + } + + private static PartBitmap Rasterize(List polygons, double cellSize, double spacingDilation) + { + if (polygons.Count == 0) + return new PartBitmap { Cells = Array.Empty(), Width = 0, Height = 0, CellSize = cellSize }; + + var minX = double.MaxValue; + var minY = double.MaxValue; + var maxX = double.MinValue; + var maxY = double.MinValue; + + foreach (var poly in polygons) + { + poly.UpdateBounds(); + var bb = poly.BoundingBox; + if (bb.Left < minX) minX = bb.Left; + if (bb.Bottom < minY) minY = bb.Bottom; + if (bb.Right > maxX) maxX = bb.Right; + if (bb.Top > maxY) maxY = bb.Top; + } + + minX -= spacingDilation; + minY -= spacingDilation; + maxX += spacingDilation; + maxY += spacingDilation; + + var width = (int)System.Math.Ceiling((maxX - minX) / cellSize); + var height = (int)System.Math.Ceiling((maxY - minY) / cellSize); + + if (width <= 0 || height <= 0) + return new PartBitmap { Cells = Array.Empty(), Width = 0, Height = 0, CellSize = cellSize }; + + var cells = new int[width * height]; + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var px = minX + (x + 0.5) * cellSize; + var py = minY + (y + 0.5) * cellSize; + var pt = new Vector(px, py); + + foreach (var poly in polygons) + { + if (poly.ContainsPoint(pt)) + { + cells[y * width + x] = 1; + break; + } + } + } + } + + var dilationCells = (int)System.Math.Ceiling(spacingDilation / cellSize); + + if (dilationCells > 0) + Dilate(cells, width, height, dilationCells); + + return new PartBitmap + { + Cells = cells, + Width = width, + Height = height, + CellSize = cellSize, + OriginX = minX, + OriginY = minY + }; + } + + private static List GetClosedPolygons(Drawing drawing) + { + var entities = ConvertProgram.ToGeometry(drawing.Program) + .Where(e => e.Layer != SpecialLayers.Rapid); + var shapes = Helper.GetShapes(entities); + + var polygons = new List(); + + foreach (var shape in shapes) + { + if (!shape.IsClosed()) + continue; + + var polygon = shape.ToPolygonWithTolerance(0.05); + polygon.Close(); + polygons.Add(polygon); + } + + return polygons; + } + + private static void Dilate(int[] cells, int width, int height, int radius) + { + var source = (int[])cells.Clone(); + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + if (source[y * width + x] != 1) + continue; + + for (var dy = -radius; dy <= radius; dy++) + { + for (var dx = -radius; dx <= radius; dx++) + { + var nx = x + dx; + var ny = y + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) + cells[ny * width + nx] = 1; + } + } + } + } + } + } +} diff --git a/OpenNest.sln b/OpenNest.sln index d26a394..251ebd6 100644 --- a/OpenNest.sln +++ b/OpenNest.sln @@ -9,24 +9,66 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Core", "OpenNest.C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Engine", "OpenNest.Engine\OpenNest.Engine.csproj", "{0083B9CC-54AD-4085-A30D-56BC6834B71A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Gpu", "OpenNest.Gpu\OpenNest.Gpu.csproj", "{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|x64.Build.0 = Debug|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Debug|x86.Build.0 = Debug|Any CPU {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|Any CPU.Build.0 = Release|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|x64.ActiveCfg = Release|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|x64.Build.0 = Release|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|x86.ActiveCfg = Release|Any CPU + {1F1E40E0-5C53-474F-A258-69C9C3FAC15A}.Release|x86.Build.0 = Release|Any CPU {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|x64.Build.0 = Debug|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Debug|x86.Build.0 = Debug|Any CPU {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|Any CPU.Build.0 = Release|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|x64.ActiveCfg = Release|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|x64.Build.0 = Release|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|x86.ActiveCfg = Release|Any CPU + {5A5FDE8D-F8DB-440E-866C-C4807E1686CF}.Release|x86.Build.0 = Release|Any CPU {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|x64.ActiveCfg = Debug|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|x64.Build.0 = Debug|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|x86.ActiveCfg = Debug|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Debug|x86.Build.0 = Debug|Any CPU {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|Any CPU.ActiveCfg = Release|Any CPU {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|Any CPU.Build.0 = Release|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|x64.ActiveCfg = Release|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|x64.Build.0 = Release|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|x86.ActiveCfg = Release|Any CPU + {0083B9CC-54AD-4085-A30D-56BC6834B71A}.Release|x86.Build.0 = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|x64.Build.0 = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Debug|x86.Build.0 = Debug|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|Any CPU.Build.0 = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x64.ActiveCfg = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x64.Build.0 = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.ActiveCfg = Release|Any CPU + {1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 5879048..c0755a3 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -172,6 +172,7 @@ namespace OpenNest.Actions { var plate = plateView.Plate; var engine = new NestEngine(plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; var groupParts = parts.Select(p => p.BasePart).ToList(); var bounds = plate.WorkArea(); diff --git a/OpenNest/Actions/ActionFillArea.cs b/OpenNest/Actions/ActionFillArea.cs index ba64af2..c17e39f 100644 --- a/OpenNest/Actions/ActionFillArea.cs +++ b/OpenNest/Actions/ActionFillArea.cs @@ -25,10 +25,8 @@ namespace OpenNest.Actions private void FillArea() { var engine = new NestEngine(plateView.Plate); - engine.FillArea(SelectedArea, new NestItem - { - Drawing = drawing - }); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; + engine.Fill(new NestItem { Drawing = drawing }, SelectedArea); plateView.Invalidate(); Update(); diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 87218d5..e32d58d 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -59,6 +59,7 @@ this.mnuViewZoomIn = new System.Windows.Forms.ToolStripMenuItem(); this.mnuViewZoomOut = new System.Windows.Forms.ToolStripMenuItem(); this.mnuTools = new System.Windows.Forms.ToolStripMenuItem(); + this.mnuToolsBestFitViewer = new System.Windows.Forms.ToolStripMenuItem(); this.mnuToolsMeasureArea = new System.Windows.Forms.ToolStripMenuItem(); this.mnuToolsAlign = new System.Windows.Forms.ToolStripMenuItem(); this.mnuToolsAlignLeft = new System.Windows.Forms.ToolStripMenuItem(); @@ -129,6 +130,7 @@ this.plateIndexStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.plateSizeStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.plateQtyStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.gpuStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.toolStrip1 = new System.Windows.Forms.ToolStrip(); this.btnNew = new System.Windows.Forms.ToolStripButton(); this.btnOpen = new System.Windows.Forms.ToolStripButton(); @@ -412,6 +414,7 @@ // this.mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.mnuToolsMeasureArea, + this.mnuToolsBestFitViewer, this.mnuToolsAlign, this.toolStripMenuItem14, this.mnuSetOffsetIncrement, @@ -428,7 +431,14 @@ this.mnuToolsMeasureArea.Size = new System.Drawing.Size(214, 22); this.mnuToolsMeasureArea.Text = "Measure Area"; this.mnuToolsMeasureArea.Click += new System.EventHandler(this.MeasureArea_Click); - // + // + // mnuToolsBestFitViewer + // + this.mnuToolsBestFitViewer.Name = "mnuToolsBestFitViewer"; + this.mnuToolsBestFitViewer.Size = new System.Drawing.Size(214, 22); + this.mnuToolsBestFitViewer.Text = "Best-Fit Viewer"; + this.mnuToolsBestFitViewer.Click += new System.EventHandler(this.BestFitViewer_Click); + // // mnuToolsAlign // this.mnuToolsAlign.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -909,7 +919,8 @@ this.spacerLabel, this.plateIndexStatusLabel, this.plateSizeStatusLabel, - this.plateQtyStatusLabel}); + this.plateQtyStatusLabel, + this.gpuStatusLabel}); this.statusStrip1.Location = new System.Drawing.Point(0, 543); this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Size = new System.Drawing.Size(1098, 24); @@ -961,7 +972,14 @@ this.plateQtyStatusLabel.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0); this.plateQtyStatusLabel.Size = new System.Drawing.Size(55, 19); this.plateQtyStatusLabel.Text = "Qty : 0"; - // + // + // gpuStatusLabel + // + this.gpuStatusLabel.BorderSides = System.Windows.Forms.ToolStripStatusLabelBorderSides.Left; + this.gpuStatusLabel.Name = "gpuStatusLabel"; + this.gpuStatusLabel.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.gpuStatusLabel.Size = new System.Drawing.Size(55, 19); + // // toolStrip1 // this.toolStrip1.AutoSize = false; @@ -1287,7 +1305,9 @@ private System.Windows.Forms.ToolStripMenuItem manualSequenceToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem autoSequenceAllPlatesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem mnuToolsMeasureArea; + private System.Windows.Forms.ToolStripMenuItem mnuToolsBestFitViewer; private System.Windows.Forms.ToolStripButton btnSaveAs; private System.Windows.Forms.ToolStripMenuItem centerPartsToolStripMenuItem; + private System.Windows.Forms.ToolStripStatusLabel gpuStatusLabel; } } \ No newline at end of file diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 16adabe..99918fa 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -39,6 +39,7 @@ namespace OpenNest.Forms LoadPosts(); EnableCheck(); UpdateStatus(); + UpdateGpuStatus(); } private string GetNestName(DateTime date, int id) @@ -191,6 +192,20 @@ namespace OpenNest.Forms UpdatePlateStatus(); } + private void UpdateGpuStatus() + { + if (GpuEvaluatorFactory.GpuAvailable) + { + gpuStatusLabel.Text = $"GPU : {GpuEvaluatorFactory.DeviceName}"; + gpuStatusLabel.ForeColor = Color.DarkGreen; + } + else + { + gpuStatusLabel.Text = "GPU : None (CPU)"; + gpuStatusLabel.ForeColor = Color.Gray; + } + } + private void UpdateLocationMode() { if (activeForm == null) @@ -501,6 +516,33 @@ namespace OpenNest.Forms activeForm.PlateView.SetAction(typeof(ActionSelectArea)); } + private void BestFitViewer_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var plate = activeForm.PlateView.Plate; + var drawing = activeForm.Nest.Drawings.Count > 0 + ? activeForm.Nest.Drawings.First() + : null; + + if (drawing == null) + { + MessageBox.Show("No drawings available.", "Best-Fit Viewer", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + using (var form = new BestFitViewerForm(drawing, plate)) + { + if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) + { + var parts = NestEngine.BuildPairParts(form.SelectedResult, drawing); + activeForm.PlateView.SetAction(typeof(ActionClone), parts); + } + } + } + private void SetOffsetIncrement_Click(object sender, EventArgs e) { if (activeForm == null) return; @@ -645,6 +687,7 @@ namespace OpenNest.Forms : activeForm.PlateView.Plate; var engine = new NestEngine(plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; if (!engine.Pack(items)) break; @@ -718,6 +761,7 @@ namespace OpenNest.Forms return; var engine = new NestEngine(activeForm.PlateView.Plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; engine.Fill(new NestItem { Drawing = drawing diff --git a/OpenNest/GpuEvaluatorFactory.cs b/OpenNest/GpuEvaluatorFactory.cs new file mode 100644 index 0000000..ca94ef8 --- /dev/null +++ b/OpenNest/GpuEvaluatorFactory.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics; +using ILGPU; +using ILGPU.Runtime; +using OpenNest.Engine.BestFit; +using OpenNest.Gpu; + +namespace OpenNest +{ + internal static class GpuEvaluatorFactory + { + private static bool _probed; + private static bool _gpuAvailable; + private static string _deviceName; + + public static bool GpuAvailable + { + get + { + if (!_probed) Probe(); + return _gpuAvailable; + } + } + + public static string DeviceName + { + get + { + if (!_probed) Probe(); + return _deviceName ?? "None"; + } + } + + public static IPairEvaluator Create(Drawing drawing, double spacing) + { + if (!GpuAvailable) + return null; + + try + { + return new GpuPairEvaluator(drawing, spacing); + } + catch (Exception ex) + { + Debug.WriteLine($"[GpuEvaluatorFactory] GPU evaluator failed: {ex.Message}"); + return null; + } + } + + private static void Probe() + { + _probed = true; + + try + { + using var context = Context.CreateDefault(); + foreach (var device in context.Devices) + { + if (device.AcceleratorType == AcceleratorType.Cuda || + device.AcceleratorType == AcceleratorType.OpenCL) + { + _gpuAvailable = true; + _deviceName = device.Name; + Debug.WriteLine($"[GpuEvaluatorFactory] GPU found: {device.Name} ({device.AcceleratorType})"); + return; + } + } + + Debug.WriteLine("[GpuEvaluatorFactory] No GPU device found"); + } + catch (Exception ex) + { + Debug.WriteLine($"[GpuEvaluatorFactory] GPU probe failed: {ex.Message}"); + } + } + } +} diff --git a/OpenNest/OpenNest.csproj b/OpenNest/OpenNest.csproj index d3a7593..4eafc18 100644 --- a/OpenNest/OpenNest.csproj +++ b/OpenNest/OpenNest.csproj @@ -13,6 +13,7 @@ +