An advanced sprite packing tool. Currently a work in progress.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SpritePacker.cs 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Security.Cryptography;
  5. using System.IO;
  6. using System.Configuration;
  7. using System.Drawing.Text;
  8. using System.Drawing.Imaging;
  9. using System.Text;
  10. using System.Linq;
  11. namespace SpritePacker
  12. {
  13. public class SpritePacker
  14. {
  15. /// <summary>
  16. /// A list of all the sprites added to the sprite packer.
  17. /// </summary>
  18. private List<Sprite> sprites = new List<Sprite>();
  19. /// <summary>
  20. /// Whether debug information should be outputted to the console.
  21. /// </summary>
  22. /// <value><c>true</c> if verbose; <c>false</c> otherwise.</value>
  23. public bool Verbose { get; private set; }
  24. /// <summary>
  25. /// Gets a list of the current sprites.
  26. /// </summary>
  27. public Sprite[] CurrentSprites
  28. {
  29. get {
  30. return sprites.ToArray();
  31. }
  32. }
  33. /// <summary>
  34. /// Initializes a new SpritePacker.
  35. /// </summary>
  36. /// <param name="inVerbose">Whether to output debug information to the console.</param>
  37. public SpritePacker(bool inVerbose = false)
  38. {
  39. Verbose = inVerbose;
  40. }
  41. /// <summary>
  42. /// Adds a sprite to the sprite packer.
  43. /// </summary>
  44. /// <param name="sprite">Sprite.</param>
  45. public void Add(Sprite sprite)
  46. {
  47. if(Verbose) Console.WriteLine("Adding {0}.", sprite);
  48. sprites.Add(sprite);
  49. }
  50. public void Add(IEnumerable<Sprite> sprites)
  51. {
  52. foreach (Sprite sprite in sprites)
  53. Add(sprite);
  54. }
  55. /// <summary>
  56. /// Adds a sprite to the sprite packer.
  57. /// </summary>
  58. /// <param name="sprite">Whether a sprite was actually removed or not.</param>
  59. public bool Remove(Sprite sprite)
  60. {
  61. return sprites.Remove(sprite);
  62. }
  63. /// <summary>
  64. /// Clears the list of sprites added to the current sprite packer.
  65. /// </summary>
  66. public void Clear()
  67. {
  68. sprites.Clear();
  69. }
  70. /// <summary>
  71. /// Packs all the added sprites in as small a area as possible.
  72. /// Note that this operation may potentially be very computationally expensive.
  73. /// </summary>
  74. public void Arrange()
  75. {
  76. sortBySize();
  77. List<Sprite> arrangedSprites = sprites.Where((Sprite spr) => spr.Placed).ToList();
  78. List<Sprite> spritesToPack = sprites.Where((Sprite spr) => !spr.Placed).ToList();
  79. foreach(Sprite cspr in spritesToPack)
  80. {
  81. if(Verbose) Console.WriteLine("Attempting to place {0}.", cspr);
  82. Point scanLines = Point.Empty;
  83. Point nextScanLines = new Point(int.MaxValue, int.MaxValue);
  84. while(true)
  85. {
  86. if (!cspr.IntersectsWith(arrangedSprites))
  87. break;
  88. if(Verbose) Console.WriteLine("Scan lines: {0}", scanLines);
  89. if(Verbose) Console.WriteLine("Scanning X...");
  90. // Scan along the X axis
  91. cspr.X = 0;
  92. cspr.Y = scanLines.Y;
  93. bool foundPosition = false;
  94. while(cspr.X <= scanLines.X)
  95. {
  96. if(Verbose) Console.Write("Position: {0} ", cspr.X);
  97. if (!cspr.IntersectsWith(arrangedSprites))
  98. {
  99. if(Verbose) Console.WriteLine("Found position on the X axis.");
  100. foundPosition = true;
  101. break;
  102. }
  103. // Get the edge furthest to the right
  104. List<Sprite> problems = cspr.GetIntersectors(arrangedSprites);
  105. Sprite rightProblem = problems[0];
  106. foreach (Sprite probSpr in problems)
  107. {
  108. if (probSpr.Right > rightProblem.Right)
  109. rightProblem = probSpr;
  110. // If the current problem's bottom edge is less than the bottom of the next scan line,
  111. // move the next scan line up a bit.
  112. // Also make sure that the next scan line and the current scan line don't touch or cross.
  113. if (probSpr.Bottom < nextScanLines.Y && probSpr.Bottom > scanLines.Y)
  114. nextScanLines.Y = probSpr.Bottom; // NOTE: Add one here?
  115. }
  116. if(Verbose) Console.WriteLine("Found rightmost problem: {0}", rightProblem);
  117. // Move up to the position furthest to the right
  118. cspr.X = rightProblem.Right; // NOTE: Add one here?
  119. }
  120. if (!foundPosition)
  121. {
  122. if(Verbose) Console.WriteLine("Failed to find anything on the X axis. Scanning Y...");
  123. // We didn't find anything along the x axis - let's scan the y axis next
  124. cspr.X = scanLines.X;
  125. cspr.Y = 0;
  126. while (cspr.Y <= scanLines.Y)
  127. {
  128. if(Verbose) Console.Write("Position: {0} ", cspr.Y);
  129. if (!cspr.IntersectsWith(arrangedSprites))
  130. {
  131. if(Verbose) Console.WriteLine("Found position on the Y axis.");
  132. foundPosition = true;
  133. break;
  134. }
  135. // Get the edge furthest downwards
  136. List<Sprite> problems = cspr.GetIntersectors(arrangedSprites);
  137. Sprite downProblem = problems[0];
  138. foreach (Sprite probSpr in problems)
  139. {
  140. if (probSpr.Bottom > downProblem.Bottom)
  141. downProblem = probSpr;
  142. // If the current problem's right edge is further in than the current next scan line,
  143. // move the next scan line up to meet it.
  144. // Also make sure that the next scan line and the current scan line don't touch or cross.
  145. if (probSpr.Right < nextScanLines.X && probSpr.Right > scanLines.X)
  146. nextScanLines.X = probSpr.Right; // NOTE: Add one here?
  147. }
  148. if(Verbose) Console.WriteLine("Found downProblem {0}", downProblem);
  149. // Move up to the position furthest downwards
  150. cspr.Y = downProblem.Bottom; // NOTE: Add one here?
  151. }
  152. }
  153. // If we found a new position, then we don't need to move the scan lines up and try again
  154. if (foundPosition) {
  155. cspr.Placed = true;
  156. break;
  157. }
  158. if(Verbose) Console.WriteLine("Failed to find a position along the current scan lines.");
  159. if(Verbose) Console.WriteLine("Next candidate scan lines: {0}", nextScanLines);
  160. // Make sure that the next scan lines are sane
  161. if (nextScanLines.X == int.MaxValue)
  162. nextScanLines.X = scanLines.X;
  163. if (nextScanLines.Y == int.MaxValue)
  164. nextScanLines.Y = scanLines.Y;
  165. if(Verbose) Console.WriteLine("Actual next scan lines: {0}", nextScanLines);
  166. // If the next scan lines and the current scan lines are identical,
  167. // then something is very wrong
  168. if(nextScanLines.Equals(scanLines))
  169. throw new Exception("Failed to find the next set of lines to scan!");
  170. // Move the scan lines up to the next nearest ones we've found
  171. scanLines = nextScanLines;
  172. nextScanLines = new Point(int.MaxValue, int.MaxValue);
  173. }
  174. arrangedSprites.Add(cspr);
  175. if(Verbose) Console.WriteLine("Finished positioning {0}.", cspr);
  176. }
  177. // We don't need to copy the list of arranged sprites across to the main list here
  178. // because Sprite is a class and classes are passed by _reference_.
  179. }
  180. /// <summary>
  181. /// Generates an image that contains all the currently added sprites.
  182. /// You probably want to call Arrage() before calling this method.
  183. /// Returns <c>null</c> if you haven't added any sprites to the SpritePacker yet.
  184. /// </summary>
  185. /// <returns>The generated image.</returns>
  186. public Bitmap GenerateImage()
  187. {
  188. // Calculate the size of the image we are about to output
  189. Point imageSize = new Point(0, 0);
  190. foreach(Sprite spr in sprites)
  191. {
  192. if (spr.Bottom > imageSize.Y)
  193. imageSize.Y = spr.Bottom;
  194. if (spr.Right > imageSize.X)
  195. imageSize.X = spr.Right;
  196. }
  197. if (imageSize.IsEmpty)
  198. return null;
  199. Bitmap finalImage = new Bitmap(imageSize.X, imageSize.Y, PixelFormat.Format32bppArgb);
  200. finalImage.MakeTransparent();
  201. using (Graphics context = Graphics.FromImage(finalImage))
  202. {
  203. foreach(Sprite spr in sprites)
  204. {
  205. context.DrawImage(spr.Image, spr.Location);
  206. }
  207. }
  208. return finalImage;
  209. }
  210. /// <summary>
  211. /// Output the packed sprite as an image to the specified filename.
  212. /// You probably want to call Arrage() before calling this method.
  213. /// </summary>
  214. /// <param name="outputFilename">The filename to save the generated image to.</param>
  215. public void Output(string outputFilename)
  216. {
  217. using (Bitmap finalImage = GenerateImage())
  218. {
  219. finalImage.Save(outputFilename);
  220. }
  221. }
  222. /// <summary>
  223. /// Gets a the details of the currently added sprites as a string of CSV.
  224. /// </summary>
  225. /// <returns>Details of the current sprites as a string of CSV.</returns>
  226. /// <param name="header">Whether to include a header in the generated CSV.</param>
  227. public string GetSpritePositionsCSV(bool header = true)
  228. {
  229. StringWriter result = new StringWriter();
  230. if (header)
  231. result.WriteLine("index,filename,x,y,width,height");
  232. int i = 0;
  233. foreach(Sprite spr in sprites)
  234. {
  235. result.WriteLine("{0},{1},{2},{3},{4},{5}", new object[] { i, spr.Filename, spr.X, spr.Y, spr.Width, spr.Height });
  236. i++;
  237. }
  238. return result.ToString();
  239. }
  240. /// <summary>
  241. /// Sorts the sprites by size.
  242. /// </summary>
  243. private void sortBySize()
  244. {
  245. sprites.Sort((a, b) => -a.AreaSize.CompareTo(b.AreaSize));
  246. }
  247. /// <summary>
  248. /// Returns a string that represents the current sprite packer.
  249. /// </summary>
  250. /// <returns>A string that represents the current sprite packer.</returns>
  251. public override string ToString()
  252. {
  253. string result = string.Format("SpritePacker:") + Environment.NewLine;
  254. foreach (Sprite spr in sprites)
  255. result += string.Format("\t{0}\n", spr);
  256. return result;
  257. }
  258. }
  259. }