Atlas.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated July 28, 2023. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2023, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software or
  13. * otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
  27. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. #if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1)
  30. #define IS_UNITY
  31. #endif
  32. using System;
  33. using System.Collections.Generic;
  34. using System.Globalization;
  35. using System.IO;
  36. using System.Reflection;
  37. #if WINDOWS_STOREAPP
  38. using System.Threading.Tasks;
  39. using Windows.Storage;
  40. #endif
  41. namespace Spine {
  42. public class Atlas : IEnumerable<AtlasRegion> {
  43. readonly List<AtlasPage> pages = new List<AtlasPage>();
  44. List<AtlasRegion> regions = new List<AtlasRegion>();
  45. TextureLoader textureLoader;
  46. #region IEnumerable implementation
  47. public IEnumerator<AtlasRegion> GetEnumerator () {
  48. return regions.GetEnumerator();
  49. }
  50. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () {
  51. return regions.GetEnumerator();
  52. }
  53. #endregion
  54. public List<AtlasRegion> Regions { get { return regions; } }
  55. public List<AtlasPage> Pages { get { return pages; } }
  56. #if !(IS_UNITY)
  57. #if WINDOWS_STOREAPP
  58. private async Task ReadFile (string path, TextureLoader textureLoader) {
  59. var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
  60. var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false);
  61. using (StreamReader reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) {
  62. try {
  63. Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader);
  64. this.pages = atlas.pages;
  65. this.regions = atlas.regions;
  66. this.textureLoader = atlas.textureLoader;
  67. } catch (Exception ex) {
  68. throw new Exception("Error reading atlas file: " + path, ex);
  69. }
  70. }
  71. }
  72. public Atlas (string path, TextureLoader textureLoader) {
  73. this.ReadFile(path, textureLoader).Wait();
  74. }
  75. #else
  76. public Atlas (string path, TextureLoader textureLoader) {
  77. #if WINDOWS_PHONE
  78. Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path);
  79. using (StreamReader reader = new StreamReader(stream)) {
  80. #else
  81. using (StreamReader reader = new StreamReader(path)) {
  82. #endif // WINDOWS_PHONE
  83. try {
  84. Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader);
  85. this.pages = atlas.pages;
  86. this.regions = atlas.regions;
  87. this.textureLoader = atlas.textureLoader;
  88. } catch (Exception ex) {
  89. throw new Exception("Error reading atlas file: " + path, ex);
  90. }
  91. }
  92. }
  93. #endif // WINDOWS_STOREAPP
  94. #endif
  95. public Atlas (List<AtlasPage> pages, List<AtlasRegion> regions) {
  96. if (pages == null) throw new ArgumentNullException("pages", "pages cannot be null.");
  97. if (regions == null) throw new ArgumentNullException("regions", "regions cannot be null.");
  98. this.pages = pages;
  99. this.regions = regions;
  100. this.textureLoader = null;
  101. }
  102. public Atlas (TextReader reader, string imagesDir, TextureLoader textureLoader) {
  103. if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null.");
  104. if (imagesDir == null) throw new ArgumentNullException("imagesDir", "imagesDir cannot be null.");
  105. if (textureLoader == null) throw new ArgumentNullException("textureLoader", "textureLoader cannot be null.");
  106. this.textureLoader = textureLoader;
  107. string[] entry = new string[5];
  108. AtlasPage page = null;
  109. AtlasRegion region = null;
  110. Dictionary<string, Action> pageFields = new Dictionary<string, Action>(5);
  111. pageFields.Add("size", () => {
  112. page.width = int.Parse(entry[1], CultureInfo.InvariantCulture);
  113. page.height = int.Parse(entry[2], CultureInfo.InvariantCulture);
  114. });
  115. pageFields.Add("format", () => {
  116. page.format = (Format)Enum.Parse(typeof(Format), entry[1], false);
  117. });
  118. pageFields.Add("filter", () => {
  119. page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[1], false);
  120. page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[2], false);
  121. });
  122. pageFields.Add("repeat", () => {
  123. if (entry[1].IndexOf('x') != -1) page.uWrap = TextureWrap.Repeat;
  124. if (entry[1].IndexOf('y') != -1) page.vWrap = TextureWrap.Repeat;
  125. });
  126. pageFields.Add("pma", () => {
  127. page.pma = entry[1] == "true";
  128. });
  129. Dictionary<string, Action> regionFields = new Dictionary<string, Action>(8);
  130. regionFields.Add("xy", () => { // Deprecated, use bounds.
  131. region.x = int.Parse(entry[1], CultureInfo.InvariantCulture);
  132. region.y = int.Parse(entry[2], CultureInfo.InvariantCulture);
  133. });
  134. regionFields.Add("size", () => { // Deprecated, use bounds.
  135. region.width = int.Parse(entry[1], CultureInfo.InvariantCulture);
  136. region.height = int.Parse(entry[2], CultureInfo.InvariantCulture);
  137. });
  138. regionFields.Add("bounds", () => {
  139. region.x = int.Parse(entry[1], CultureInfo.InvariantCulture);
  140. region.y = int.Parse(entry[2], CultureInfo.InvariantCulture);
  141. region.width = int.Parse(entry[3], CultureInfo.InvariantCulture);
  142. region.height = int.Parse(entry[4], CultureInfo.InvariantCulture);
  143. });
  144. regionFields.Add("offset", () => { // Deprecated, use offsets.
  145. region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture);
  146. region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture);
  147. });
  148. regionFields.Add("orig", () => { // Deprecated, use offsets.
  149. region.originalWidth = int.Parse(entry[1], CultureInfo.InvariantCulture);
  150. region.originalHeight = int.Parse(entry[2], CultureInfo.InvariantCulture);
  151. });
  152. regionFields.Add("offsets", () => {
  153. region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture);
  154. region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture);
  155. region.originalWidth = int.Parse(entry[3], CultureInfo.InvariantCulture);
  156. region.originalHeight = int.Parse(entry[4], CultureInfo.InvariantCulture);
  157. });
  158. regionFields.Add("rotate", () => {
  159. string value = entry[1];
  160. if (value == "true")
  161. region.degrees = 90;
  162. else if (value != "false")
  163. region.degrees = int.Parse(value, CultureInfo.InvariantCulture);
  164. });
  165. regionFields.Add("index", () => {
  166. region.index = int.Parse(entry[1], CultureInfo.InvariantCulture);
  167. });
  168. string line = reader.ReadLine();
  169. // Ignore empty lines before first entry.
  170. while (line != null && line.Trim().Length == 0)
  171. line = reader.ReadLine();
  172. // Header entries.
  173. while (true) {
  174. if (line == null || line.Trim().Length == 0) break;
  175. if (ReadEntry(entry, line) == 0) break; // Silently ignore all header fields.
  176. line = reader.ReadLine();
  177. }
  178. // Page and region entries.
  179. List<string> names = null;
  180. List<int[]> values = null;
  181. while (true) {
  182. if (line == null) break;
  183. if (line.Trim().Length == 0) {
  184. page = null;
  185. line = reader.ReadLine();
  186. } else if (page == null) {
  187. page = new AtlasPage();
  188. page.name = line.Trim();
  189. while (true) {
  190. if (ReadEntry(entry, line = reader.ReadLine()) == 0) break;
  191. Action field;
  192. if (pageFields.TryGetValue(entry[0], out field)) field(); // Silently ignore unknown page fields.
  193. }
  194. textureLoader.Load(page, Path.Combine(imagesDir, page.name));
  195. pages.Add(page);
  196. } else {
  197. region = new AtlasRegion();
  198. region.page = page;
  199. region.name = line;
  200. while (true) {
  201. int count = ReadEntry(entry, line = reader.ReadLine());
  202. if (count == 0) break;
  203. Action field;
  204. if (regionFields.TryGetValue(entry[0], out field))
  205. field();
  206. else {
  207. if (names == null) {
  208. names = new List<string>(8);
  209. values = new List<int[]>(8);
  210. }
  211. names.Add(entry[0]);
  212. int[] entryValues = new int[count];
  213. for (int i = 0; i < count; i++)
  214. int.TryParse(entry[i + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out entryValues[i]); // Silently ignore non-integer values.
  215. values.Add(entryValues);
  216. }
  217. }
  218. if (region.originalWidth == 0 && region.originalHeight == 0) {
  219. region.originalWidth = region.width;
  220. region.originalHeight = region.height;
  221. }
  222. if (names != null && names.Count > 0) {
  223. region.names = names.ToArray();
  224. region.values = values.ToArray();
  225. names.Clear();
  226. values.Clear();
  227. }
  228. region.u = region.x / (float)page.width;
  229. region.v = region.y / (float)page.height;
  230. if (region.degrees == 90) {
  231. region.u2 = (region.x + region.height) / (float)page.width;
  232. region.v2 = (region.y + region.width) / (float)page.height;
  233. int tempSwap = region.packedWidth;
  234. region.packedWidth = region.packedHeight;
  235. region.packedHeight = tempSwap;
  236. } else {
  237. region.u2 = (region.x + region.width) / (float)page.width;
  238. region.v2 = (region.y + region.height) / (float)page.height;
  239. }
  240. regions.Add(region);
  241. }
  242. }
  243. }
  244. static private int ReadEntry (string[] entry, string line) {
  245. if (line == null) return 0;
  246. line = line.Trim();
  247. if (line.Length == 0) return 0;
  248. int colon = line.IndexOf(':');
  249. if (colon == -1) return 0;
  250. entry[0] = line.Substring(0, colon).Trim();
  251. for (int i = 1, lastMatch = colon + 1; ; i++) {
  252. int comma = line.IndexOf(',', lastMatch);
  253. if (comma == -1) {
  254. entry[i] = line.Substring(lastMatch).Trim();
  255. return i;
  256. }
  257. entry[i] = line.Substring(lastMatch, comma - lastMatch).Trim();
  258. lastMatch = comma + 1;
  259. if (i == 4) return 4;
  260. }
  261. }
  262. public void FlipV () {
  263. for (int i = 0, n = regions.Count; i < n; i++) {
  264. AtlasRegion region = regions[i];
  265. region.v = 1 - region.v;
  266. region.v2 = 1 - region.v2;
  267. }
  268. }
  269. /// <summary>Returns the first region found with the specified name. This method uses string comparison to find the region, so the result
  270. /// should be cached rather than calling this method multiple times.</summary>
  271. /// <returns>The region, or null.</returns>
  272. public AtlasRegion FindRegion (string name) {
  273. for (int i = 0, n = regions.Count; i < n; i++)
  274. if (regions[i].name == name) return regions[i];
  275. return null;
  276. }
  277. public void Dispose () {
  278. if (textureLoader == null) return;
  279. for (int i = 0, n = pages.Count; i < n; i++)
  280. textureLoader.Unload(pages[i].rendererObject);
  281. }
  282. }
  283. public enum Format {
  284. Alpha,
  285. Intensity,
  286. LuminanceAlpha,
  287. RGB565,
  288. RGBA4444,
  289. RGB888,
  290. RGBA8888
  291. }
  292. public enum TextureFilter {
  293. Nearest,
  294. Linear,
  295. MipMap,
  296. MipMapNearestNearest,
  297. MipMapLinearNearest,
  298. MipMapNearestLinear,
  299. MipMapLinearLinear
  300. }
  301. public enum TextureWrap {
  302. MirroredRepeat,
  303. ClampToEdge,
  304. Repeat
  305. }
  306. public class AtlasPage {
  307. public string name;
  308. public int width, height;
  309. public Format format = Format.RGBA8888;
  310. public TextureFilter minFilter = TextureFilter.Nearest;
  311. public TextureFilter magFilter = TextureFilter.Nearest;
  312. public TextureWrap uWrap = TextureWrap.ClampToEdge;
  313. public TextureWrap vWrap = TextureWrap.ClampToEdge;
  314. public bool pma;
  315. public object rendererObject;
  316. public AtlasPage Clone () {
  317. return MemberwiseClone() as AtlasPage;
  318. }
  319. }
  320. public class AtlasRegion : TextureRegion {
  321. public AtlasPage page;
  322. public string name;
  323. public int x, y;
  324. public float offsetX, offsetY;
  325. public int originalWidth, originalHeight;
  326. public int packedWidth { get { return width; } set { width = value; } }
  327. public int packedHeight { get { return height; } set { height = value; } }
  328. public int degrees;
  329. public bool rotate;
  330. public int index;
  331. public string[] names;
  332. public int[][] values;
  333. override public int OriginalWidth { get { return originalWidth; } }
  334. override public int OriginalHeight { get { return originalHeight; } }
  335. public AtlasRegion Clone () {
  336. return MemberwiseClone() as AtlasRegion;
  337. }
  338. }
  339. public interface TextureLoader {
  340. void Load (AtlasPage page, string path);
  341. void Unload (Object texture);
  342. }
  343. }