/// <summary> /// ALL parts must fit for an acceptable solution - we use this to check if a given combination of parts will fit on a board. /// </summary> /// <param name="parts"></param> /// <param name="stock"></param> /// <returns></returns> private static Solution PackALL(PartList parts, BoardList stock) { Solution sol = new Solution(); PartList mycopyofParts = parts.Copy(); Part iPart = mycopyofParts[0]; mycopyofParts.RemoveAt(0); int stockcount = stock.Count; for (int i = 0; i < stockcount; i++) { Item iStock = stock[i]; if (!iStock.TrySplit(iPart, out Item H1, out Item H2, out Item V1, out Item V2)) { continue; } if (mycopyofParts.Count == 0) { sol.Add(iPart, iStock); return(sol); } BoardList myVstock = stock.Copy(); myVstock.Remove(iStock); myVstock.AddRange(V2, V1); Solution solV = PackALL(mycopyofParts, myVstock); if (solV != null) { sol.Add(iPart, iStock); sol.AddRange(solV); return(sol); } BoardList myHstock = stock.Copy(); myHstock.Remove(iStock); myHstock.AddRange(H2, H1); Solution solH = PackALL(mycopyofParts, myHstock); if (solH != null) { sol.Add(iPart, iStock); sol.AddRange(solH); return(sol); } } return(null); }
public Bitmap Draw(BoardList stock) { double xOffset = 0; double imageHeight = 0; double boardSpacing = 70; double xMargin = 50; double yMargin = 50; double imageWidth = 2 * xMargin - boardSpacing; Font font = new Font(new FontFamily("Consolas"), 20.0f); foreach (var iBoard in stock) { if (iBoard.Length > imageHeight) { imageHeight = iBoard.Length; } imageWidth += iBoard.Width + boardSpacing; } imageHeight += 2 * yMargin; Bitmap bitmap = new Bitmap((int)imageWidth, (int)imageHeight); Graphics g = Graphics.FromImage(bitmap); g.FillRectangle(Brushes.Black, 0, 0, (int)imageWidth, (int)imageHeight); xOffset = xMargin; foreach (var iBoard in stock) { g.FillRectangle(Brushes.DarkRed, (float)(xOffset), (float)yMargin, (float)iBoard.Width, (float)iBoard.Length); string boardheader = $"{iBoard.Name} [{iBoard.Length}x{iBoard.Width}]"; SizeF textSizeBoard = g.MeasureString(boardheader, font); g.DrawString(boardheader, font, Brushes.White, (float)(xOffset + iBoard.Width / 2 - textSizeBoard.Width / 2), (float)(yMargin / 2 - textSizeBoard.Height / 2)); foreach (var iPlacement in this.Where(t => t.Stock.Name.StartsWith(iBoard.Name))) { g.FillRectangle(Brushes.Green, (float)(xOffset + iPlacement.Stock.dWidth), (float)(iPlacement.Stock.dLength + yMargin), (float)iPlacement.Part.Width, (float)iPlacement.Part.Length); g.TranslateTransform((float)(xOffset + iPlacement.Stock.dWidth + iPlacement.Part.Width / 2), (float)(iPlacement.Stock.dLength + iPlacement.Part.Length / 2 + yMargin)); g.RotateTransform(-90); string text = $"{iPlacement.Part.Name} [{iPlacement.Part.Length}x{iPlacement.Part.Width}]"; SizeF textSize = g.MeasureString(text, font); g.DrawString(text, font, Brushes.White, -(textSize.Width / 2), -(textSize.Height / 2)); g.RotateTransform(90); g.TranslateTransform(-((float)xOffset + (float)(iPlacement.Stock.dWidth + iPlacement.Part.Width / 2)), -((float)(iPlacement.Stock.dLength + iPlacement.Part.Length / 2 + yMargin))); } xOffset += iBoard.Width + boardSpacing; } g.Flush(); return(bitmap); }
public void Print(BoardList stock, long durationms) { Trace.WriteLine($"Solution summary"); Trace.WriteLine($"----------------"); Trace.WriteLine($" Processing time: {durationms / 1000} s"); Trace.WriteLine($" #Boards : {stock.Count}"); Trace.WriteLine($" #Parts : {this.Count}"); Trace.WriteLine($" Total Stock : {TotalStockArea / 1000000} m\u00b2"); Trace.WriteLine($" Used Stock : {UsedStockArea / 1000000} m\u00b2"); Trace.WriteLine($" Parts placed : {PlacedArea / 1000000} m\u00b2"); Trace.WriteLine($" Waste : {(Waste / 1000000)} m\u00b2 ({(Waste / UsedStockArea):0.0 %})"); Trace.WriteLine($"Part placements:"); Trace.WriteLine($"----------------"); foreach (var iBoard in stock) { Trace.WriteLine($" Board {iBoard} ({(this.Sum(t=>t.Stock.Name == iBoard.Name? t.Part.Area : 0) / iBoard.Area * 100):0.0} %):"); foreach (var iPlcmnt in this.Where(t => t.Stock.Name.StartsWith(iBoard.Name)).OrderBy(t => t.Part.Name)) { Trace.WriteLine($" {iPlcmnt.Part.Name} [{iPlcmnt.Part.Length} x {iPlcmnt.Part.Width}] @ [{iPlcmnt.Stock.dLength}, {iPlcmnt.Stock.dWidth}]"); } } }
public static Bitmap Draw(BoardList boards, PartList parts, bool usedstockonly = true) { double xOffset = 0; double imageHeight = 0; double boardSpacing = 70; double xMargin = 50; double yMargin = 50; double imageWidth = 2 * xMargin - boardSpacing; Font boardFont = new Font(new FontFamily("Consolas"), 15.0f); // create list of boards to draw List <BoardNode> boardsToDraw = new List <BoardNode>(boards.ToArray); if (usedstockonly) { List <string> usedboardnames = parts.ToArray.Select(t => t.Container).Distinct().ToList(); boardsToDraw.RemoveAll(t => !usedboardnames.Contains(t.ID)); } // calculate width & height required for the bitmap foreach (var iBoard in boardsToDraw) { if (iBoard.Length > imageHeight) { imageHeight = iBoard.Length; } imageWidth += iBoard.Width + boardSpacing; } imageHeight += 2 * yMargin; // create bitmap Bitmap bitmap = new Bitmap((int)imageWidth, (int)imageHeight); Graphics g = Graphics.FromImage(bitmap); // fill the background with black g.FillRectangle(Brushes.Black, 0, 0, (int)imageWidth, (int)imageHeight); // loop through all the boards to be drawn xOffset = xMargin; foreach (var iBoard in boardsToDraw) { // draw the board g.FillRectangle(Brushes.DarkRed, (float)(xOffset), (float)yMargin, (float)iBoard.Width, (float)iBoard.Length); string boardheader = $"{iBoard.ID} [{iBoard.Length}x{iBoard.Width}]"; SizeF textSizeBoard = g.MeasureString(boardheader, boardFont); g.DrawString(boardheader, boardFont, Brushes.White, (float)(xOffset + iBoard.Width / 2 - textSizeBoard.Width / 2), (float)(yMargin / 2 - textSizeBoard.Height / 2)); // loop through all the parts and draw the ones on the current board string overflowtext = ""; for (PartNode iPlacement = parts.Head; iPlacement != null; iPlacement = iPlacement.Next) { // continue with next part if this part was placed on another board if (iPlacement.Container != iBoard.ID) { continue; } // draw the part g.FillRectangle(Brushes.Green, (float)(xOffset + iPlacement.dWidth), (float)(iPlacement.dLength + yMargin), (float)iPlacement.Width, (float)iPlacement.Length); // print the part text string text1 = $"{iPlacement.ID} [{iPlacement.Length} x {iPlacement.Width}]"; string text2a = $"{iPlacement.ID}"; string text2b = $"[{iPlacement.Length} x {iPlacement.Width}]"; g.TranslateTransform((float)(xOffset + iPlacement.dWidth + iPlacement.Width / 2), (float)(iPlacement.dLength + iPlacement.Length / 2 + yMargin)); g.RotateTransform(-90); int sz = 16; do { Font partFont = new Font(new FontFamily("Consolas"), --sz); SizeF textSize = g.MeasureString(text1, partFont); if (textSize.Width < iPlacement.Length && textSize.Height < iPlacement.Width) { g.DrawString(text1, partFont, Brushes.White, -(textSize.Width / 2), -(textSize.Height / 2)); break; } textSize = g.MeasureString(text2a, partFont); SizeF textSize2 = g.MeasureString(text2b, partFont); if (Math.Max(textSize.Width, textSize2.Width) < iPlacement.Length && textSize.Height + textSize2.Height < iPlacement.Width) { g.DrawString(text2a, partFont, Brushes.White, -(textSize.Width / 2), -textSize.Height); g.DrawString(text2b, partFont, Brushes.White, -(textSize2.Width / 2), 0); break; } if (textSize.Width < iPlacement.Length && textSize.Height < iPlacement.Width) { g.DrawString(text2a, partFont, Brushes.White, -(textSize.Width / 2), -(textSize.Height / 2)); overflowtext += text1 + ", "; break; } } while (sz > 1); g.RotateTransform(90); g.TranslateTransform(-((float)xOffset + (float)(iPlacement.dWidth + iPlacement.Width / 2)), -((float)(iPlacement.dLength + iPlacement.Length / 2 + yMargin))); } g.TranslateTransform((float)(xOffset + iBoard.Width), (float)(iBoard.Length + yMargin)); g.RotateTransform(-90); g.DrawString(overflowtext.TrimEnd(',', ' '), boardFont, Brushes.White, 0, 0); g.RotateTransform(90); g.TranslateTransform(-(float)(xOffset + iBoard.Width), -(float)(iBoard.Length + yMargin)); xOffset += iBoard.Width + boardSpacing; } g.Flush(); bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); return(bitmap); }
static void Main(string[] args) { #region // Gather the inputs to the solution ... double boardMargins_length = 0; double boardMargins_Width = 0; double PartPadding_Length = 0; double PartPadding_Width = 0; double SawKerf = 3.2; BoardList boards = new BoardList( //new BoardNode("A", 2100, 193), //new BoardNode("B", 2100, 150), //new BoardNode("C", 2100, 143), //new BoardNode("D", 2100, 170), //new BoardNode("E", 2100, 185), new BoardNode("F", 2100, 210), //new BoardNode("G", 2100, 135), //new BoardNode("H", 2100, 225), null ); PartList parts = new PartList( new PartNode("001", 1721.7, 100.0), new PartNode("002", 284.5, 100.0), new PartNode("003", 1721.7, 100.0), new PartNode("004", 284.5, 100.0), new PartNode("005", 955.0, 69.3), new PartNode("006", 955.0, 60.0), new PartNode("007", 955.0, 69.6), new PartNode("008", 955.0, 80.0), new PartNode("009", 955.0, 60.0), new PartNode("010", 955.0, 60.0), new PartNode("011", 310.0, 100.0), new PartNode("012", 310.0, 100.0), new PartNode("013", 310.0, 36.0), new PartNode("014", 310.0, 36.0), new PartNode("015", 354.5, 36.0), new PartNode("016", 354.5, 36.0), new PartNode("017", 299.0, 20.0), new PartNode("018", 299.0, 20.0), new PartNode("019", 299.0, 20.0), new PartNode("020", 299.0, 20.0), new PartNode("021", 327.5, 20.0), new PartNode("022", 327.5, 20.0), //new PartNode("023", 955.0, 80.0), //new PartNode("024", 310.0, 100.0), //new PartNode("025", 310.0, 100.0), //new PartNode("026", 310.0, 36.0), //new PartNode("027", 310.0, 36.0), //new PartNode("028", 354.5, 36.0), //new PartNode("029", 354.5, 36.0), //new PartNode("030", 299.0, 20.0), //new PartNode("031", 327.5, 20.0), //new PartNode("032", 327.5, 20.0), //new PartNode("033", 955.0, 80.0), //new PartNode("034", 310.0, 100.0), //new PartNode("035", 310.0, 100.0), //new PartNode("036", 310.0, 36.0), //new PartNode("037", 310.0, 36.0), //new PartNode("038", 354.5, 36.0), //new PartNode("039", 354.5, 36.0), //new PartNode("040", 299.0, 20.0), //new PartNode("041", 327.5, 20.0), //new PartNode("042", 327.5, 20.0), //new PartNode("043", 955.0, 80.0), //new PartNode("044", 310.0, 100.0), //new PartNode("045", 310.0, 100.0), //new PartNode("046", 310.0, 36.0), //new PartNode("047", 310.0, 36.0), //new PartNode("048", 354.5, 36.0), //new PartNode("049", 354.5, 36.0), //new PartNode("050", 299.0, 20.0), null ); if (args[0].StartsWith("-clp:")) { string path = args[0].Replace("-clp:", ""); if (System.IO.File.Exists(path)) { Import.FromCutlistPlusCSV(path, out parts, out boards); } } if (args[0].StartsWith("-csv:")) { string path = args[0].Replace("-csv:", ""); if (System.IO.File.Exists(path)) { Import.FromCSV(path, out parts, out boards); } } #endregion //PartNode[] partsarray = parts.ToArray; //List<PartNode> partsList = new List<PartNode>(partsarray); //Stopwatch swt = new Stopwatch(); //swt.Start(); //for (int i = 0; i < 120000000; i++) //{ // //Array // PartNode iPart; // for (int j = 0; j < partsarray.Length; j++) { iPart = partsarray[j]; } // //linked list // //for (var iPart = parts.Head; iPart != null; iPart = iPart.Next) ; // //List // //foreach (var iPart in partsList) ; // //PartNode iPart; // //for (int j = 0; j < partsList.Count; j++) iPart = partsList[j]; //} //swt.Stop(); //Trace.WriteLine(swt.ElapsedMilliseconds); #region // Print starting parameters ... Trace.WriteLine($"Packing started @ {DateTime.Now} with the following:"); Trace.WriteLine($"------------------------------------------------"); Trace.WriteLine($" Board margins (w x l) : {boardMargins_Width} mm x {boardMargins_length} mm"); Trace.WriteLine($" Part padding (w x l) : {PartPadding_Width} mm x {PartPadding_Length} mm"); Trace.WriteLine($" Saw blade kerf : {SawKerf} mm"); Trace.WriteLine($" {boards.Count} Boards:"); for (BoardNode iBoard = boards.Head; iBoard != null; iBoard = iBoard.Next) { Trace.WriteLine($"{iBoard.ID,6} [{iBoard.Length,7:0.0} x {iBoard.Width,5:0.0}]"); } Trace.WriteLine($" {parts.Count} Parts:"); for (PartNode iPart = parts.Head; iPart != null; iPart = iPart.Next) { Trace.WriteLine($"{iPart.ID,6} [{iPart.Length,7:0.0} x {iPart.Width,5:0.0}]"); } #endregion #region // Find the solution ... Stopwatch sw = new Stopwatch(); sw.Start(); var solution = Packer.Pack(parts, boards, sawkerf: SawKerf, boardMarginLength: boardMargins_length, boardMarginWidth: boardMargins_Width, partPaddingLength: PartPadding_Length, partPaddingWidth: PartPadding_Width); sw.Stop(); #endregion #region // Print the solution ... // calculate the total area of all used boards List <string> usedBoardIDs = solution.ToArray.Select(t => t.Container).Distinct().ToList(); double UsedStockArea = boards.ToArray.Where(q => usedBoardIDs.Contains(q.ID)).Sum(t => t.Area); Trace.WriteLine("Solution:"); Trace.WriteLine("----------------"); if (solution.Count < parts.Count) { Trace.WriteLine("WARNING: All parts could not be placed!\r\n"); } for (var iBoard = boards.Head; iBoard != null; iBoard = iBoard.Next) { if (iBoard.Solution == null) { Trace.WriteLine($" Board {iBoard.ID} [{iBoard.Length,6:0.0} x {iBoard.Width,5:0.0}] : not used."); } else { Trace.WriteLine($" Board {iBoard.ID} [{iBoard.Length,6:0.0} x {iBoard.Width,5:0.0}] ({(iBoard.Solution == null ? 0 : iBoard.Solution.TotalArea / iBoard.Area):00.0 %}) :\r\n{iBoard.Solution?.ToString()}"); } } Trace.WriteLine("==========================================================="); Trace.WriteLine("Solution summary"); Trace.WriteLine("----------------"); Trace.WriteLine($" Processing time: {sw.ElapsedMilliseconds,5:0} ms"); Trace.WriteLine($" Boards : {boards.Count,5:0} ({boards.TotalArea / 1000000,6:0.000} m\u00b2)"); Trace.WriteLine($" Used boards : {usedBoardIDs.Count,5:0} ({UsedStockArea / 1000000,6:0.000} m\u00b2)"); Trace.WriteLine($" Parts : {parts.Count,5:0} ({parts.TotalArea / 1000000,6:0.000} m\u00b2)"); Trace.WriteLine($" Placed parts : {solution.Count,5:0} ({solution.TotalArea / 1000000,6:0.000} m\u00b2)"); Trace.WriteLine($" Waste : {(UsedStockArea - solution.TotalArea) / UsedStockArea,7:0.0 %} ({(UsedStockArea - solution.TotalArea) / 1000000,6:0.000} m\u00b2)"); Trace.WriteLine($" Coverage : {solution.TotalArea / UsedStockArea,7:0.0 %} ({(solution.TotalArea) / 1000000,6:0.000} m\u00b2)"); #endregion #region // Draw solution to an image ... Bitmap bmp = Draw(boards, solution); bmp.Save("out.bmp"); Console.WriteLine("Launch output image (Y/N):"); string s = Console.ReadLine(); if (s.ToLower() == "y") { Process.Start("out.bmp"); } #endregion }
public async static Task <Solution> Packold(PartList parts, BoardList boards) { #region // Algorithm : ... /* * for each board * remove all parts that will not fit on board * do * Get the combos of parts that have cumalative area smaller than board, but bigger than 90% of board (we aim high...) * level=level*level * while combos.count = 0 * sort combos by cum area desc * foreach combo * if can fit on board, break * * end for each board * keep board with leaste waste * * repeat for boards and parts left * */ #endregion if (parts.Count * boards.Count == 0) { return(null); } if (!parts.All(t => boards.Any(q => q.BiggerThan(t)))) { return(null); } PartList mycopyofParts = parts.Copy(); BoardList mycopyofBoards = boards.Copy(); mycopyofParts.Sort(Part.CompareByAreaDecending); Solution CompleteSolution = new Solution() { TotalStockArea = boards.Sum(t => t.Area) }; do { double minCoverageRatio = 0.9; int boardcount = mycopyofBoards.Count; List <Solution> iSolutionSet = new List <Solution>(); object lockobj = new object(); do { List <Task> threads = new List <Task>(); Trace.WriteLine($"Getting combinations for {boardcount} boards [{string.Join(",", mycopyofBoards.Select(t => t.Name))}] and {mycopyofParts.Count} parts with at least {minCoverageRatio * 100} % coverage"); for (int j = 0; j < boardcount; j++) { // get the best solution for every board threads.Add( Task.Factory.StartNew((o) => { Item iBoard = mycopyofBoards[(int)o]; var iCombos = GetCombos(mycopyofParts, iBoard, 0, minCoverageRatio * iBoard.Area); iCombos.Sort(Combo.CompareByCumAreaDesc); Trace.WriteLine($"Finding combinations for board {iBoard.Name}"); Solution topSolution = null; Combo topCombo = iCombos.FirstOrDefault(q => (topSolution = BruteForce.Pack_async(q.AsPartList(), iBoard).Result) != null); if (topSolution != null) { Trace.WriteLine($"Best satisfactory combination for board {iBoard.Name}: [{string.Join(",", topCombo.Select(q => q.Name))}] ; coverage = {(topSolution.PlacedArea / iBoard.Area * 100):0.0} %, waste = {topSolution.Waste / 1000000} m\u00b2"); iSolutionSet.Add(topSolution); } else { Trace.WriteLine($"No satisfactory combination for board {iBoard.Name}"); } }, j) ); } Task.WaitAll(threads.ToArray()); minCoverageRatio -= minCoverageRatio * minCoverageRatio; if (minCoverageRatio < 0.5) { minCoverageRatio = 0; } } while (iSolutionSet.Count == 0); // keep the best solution iSolutionSet.Sort(Solution.CompareByWasteAscending); // check if there is another solution that did not include any of these ... foreach (var topSol in iSolutionSet) { // if none of the parts in this solution is already part of the kept solutions if (!topSol.Any(t => CompleteSolution.FirstOrDefault(q => q.Part.Name == t.Part.Name) != null)) { // add this solution to the kept solutions Item topBoard = topSol.First().Stock; Trace.WriteLine($"Keeping solution for board {topBoard.Name}: [{string.Join(",", topSol.Select(q => q.Part.Name))}] ; coverage = {(topSol.PlacedArea / topBoard.Area * 100):0.0} %, waste = {topSol.Waste / 1000000} m\u00b2"); CompleteSolution.AddRange(topSol); // remove the parts and boards topSol.ForEach(t => mycopyofParts.Remove(t.Part)); mycopyofBoards.Remove(topBoard); } } Trace.WriteLine($"{mycopyofBoards.Count} boards and {mycopyofParts.Count} parts remain"); Trace.WriteLine(""); if (mycopyofBoards.Count == 0 && mycopyofParts.Count > 0) { return(null); } } while (mycopyofParts.Count > 0); return(CompleteSolution); }