1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-25 22:18:34 +01:00

Compare commits

...

6 commits

4 changed files with 175 additions and 48 deletions

View file

@ -45,6 +45,9 @@ Fixes
- Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added - Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added
### MLEM.Data ### MLEM.Data
Additions
- Added data, from, and copy instructions to DataTextureAtlas
Improvements Improvements
- Allow data texture atlas pivots and offsets to be negative - Allow data texture atlas pivots and offsets to be negative
- Made RuntimeTexturePacker restore texture region name and pivot when packing - Made RuntimeTexturePacker restore texture region name and pivot when packing

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -5,6 +6,8 @@ using System.Text.RegularExpressions;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Misc;
using MLEM.Textures; using MLEM.Textures;
#if FNA #if FNA
using MLEM.Extensions; using MLEM.Extensions;
@ -18,15 +21,20 @@ namespace MLEM.Data {
/// </para> /// </para>
/// <para> /// <para>
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple. /// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
/// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces. /// Each texture region defined in the atlas consists of its names (where multiple names can be separated by whitespace), followed by a set of possible instructions and their arguments, also separated by whitespace.
/// The <c>loc</c> keyword defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle. /// <list type="bullet">
/// The (optional) <c>piv</c> keyword defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region. /// <item><description>The <c>loc</c> (or <c>location</c>) instruction defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.</description></item>
/// The (optional) <c>off</c> keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. /// <item><description>The (optional) <c>piv</c> (or <c>pivot</c>) instruction defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.</description></item>
/// <item><description>The (optional) <c>off</c> (of <c>offset</c>) instruction defines an offset that is added onto the location and pivot of this texture region. This is useful when duplicating a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.</description></item>
/// <item><description>The (optional and repeatable) <c>cpy</c> (or <c>copy</c>) instruction defines an additional texture region that should also be generated from the same data, but with a given offset that will be applied to the location and pivot. It requires three arguments: the copy region's name and the x and y offsets.</description></item>
/// <item><description>The (optional and repeatable) <c>dat</c> (or <c>data</c>) instruction defines a custom data point that can be added to the resulting <see cref="TextureRegion"/>'s <see cref="GenericDataHolder"/> data. It requires two arguments: the data point's name and the data point's value, the latter of which is also stored as a string value.</description></item>
/// <item><description>The (optional) <c>frm</c> (or <c>from</c>) instruction defines a texture region (defined before the current region) whose data should be copied. All data from the region will be copied, but adding additional instructions afterwards modifies the data. It requires one argument: the name of the region whose data to copy. If this instruction is used, the <c>loc</c> instruction is not required.</description></item>
/// </list>
/// </para> /// </para>
/// <example> /// <example>
/// The following entry defines a texture region with the name <c>LongTableRight</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46. /// The following entry defines a texture region with the names <c>LongTableRight</c> and <c>LongTableUp</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
/// <code> /// <code>
/// LongTableRight /// LongTableRight LongTableUp
/// loc 32 30 64 48 /// loc 32 30 64 48
/// piv 80 46 /// piv 80 46
/// </code> /// </code>
@ -69,6 +77,7 @@ namespace MLEM.Data {
/// <summary> /// <summary>
/// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file. /// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file.
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
/// </summary> /// </summary>
/// <param name="texture">The texture to use for this data texture atlas</param> /// <param name="texture">The texture to use for this data texture atlas</param>
/// <param name="content">The content manager to use for loading</param> /// <param name="content">The content manager to use for loading</param>
@ -78,45 +87,124 @@ namespace MLEM.Data {
public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) { public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) {
var info = Path.Combine(content.RootDirectory, infoPath); var info = Path.Combine(content.RootDirectory, infoPath);
string text; string text;
if (Path.IsPathRooted(info)) { try {
text = File.ReadAllText(info); if (Path.IsPathRooted(info)) {
} else { text = File.ReadAllText(info);
using (var reader = new StreamReader(TitleContainer.OpenStream(info))) } else {
text = reader.ReadToEnd(); using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
text = reader.ReadToEnd();
}
} catch (Exception e) {
throw new ContentLoadException($"Couldn't load data texture atlas data from {info}", e);
} }
var atlas = new DataTextureAtlas(texture); var atlas = new DataTextureAtlas(texture);
var words = Regex.Split(text, @"\s+");
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]" var namesOffsets = new List<(string, Vector2)>();
foreach (Match match in Regex.Matches(text, @"(.+)\s+loc\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s*(?:piv\s+([0-9.+-]+)\s+([0-9.+-]+))?\s*(?:off\s+([0-9.+-]+)\s+([0-9.+-]+))?")) { var customData = new Dictionary<string, string>();
// offset var location = Rectangle.Empty;
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2( var pivot = Vector2.Zero;
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture), var offset = Vector2.Zero;
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture)); for (var i = 0; i < words.Length; i++) {
var word = words[i];
try {
switch (word) {
case "loc":
case "location":
location = new Rectangle(
int.Parse(words[i + 1], CultureInfo.InvariantCulture), int.Parse(words[i + 2], CultureInfo.InvariantCulture),
int.Parse(words[i + 3], CultureInfo.InvariantCulture), int.Parse(words[i + 4], CultureInfo.InvariantCulture));
i += 4;
break;
case "piv":
case "pivot":
pivot = new Vector2(
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
i += 2;
break;
case "off":
case "offset":
offset = new Vector2(
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
i += 2;
break;
case "cpy":
case "copy":
var copyOffset = new Vector2(
float.Parse(words[i + 2], CultureInfo.InvariantCulture),
float.Parse(words[i + 3], CultureInfo.InvariantCulture));
namesOffsets.Add((words[i + 1], copyOffset));
i += 3;
break;
case "dat":
case "data":
customData.Add(words[i + 1], words[i + 2]);
i += 2;
break;
case "frm":
case "from":
var fromRegion = atlas[words[i + 1]];
customData.Clear();
foreach (var key in fromRegion.GetDataKeys())
customData.Add(key, fromRegion.GetData<string>(key));
// our main texture might be a sub-region already, so we have to take that into account
location = fromRegion.Area.OffsetCopy(new Point(-texture.U, -texture.V));
pivot = fromRegion.PivotPixels;
if (pivot != Vector2.Zero && !pivotRelative)
pivot += location.Location.ToVector2();
offset = Vector2.Zero;
i += 1;
break;
default:
// if we have data for the previous regions, they're valid so we add them
AddCurrentRegions();
// location // we're starting a new region (or adding another name for a new region), so clear old data
var loc = new Rectangle( namesOffsets.Add((word.Trim(), Vector2.Zero));
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value), customData.Clear();
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value)); location = Rectangle.Empty;
loc.Offset(off.ToPoint()); pivot = Vector2.Zero;
offset = Vector2.Zero;
// pivot break;
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2( }
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X), } catch (Exception e) {
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)); throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e);
foreach (var name in Regex.Split(match.Groups[1].Value, @"\s")) {
var trimmed = name.Trim();
if (trimmed.Length <= 0)
continue;
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = trimmed
};
atlas.regions.Add(trimmed, region);
} }
} }
// add the last region that was started on
AddCurrentRegions();
return atlas; return atlas;
void AddCurrentRegions() {
// the location is the only mandatory information, which is why we check it here
if (location == Rectangle.Empty || namesOffsets.Count <= 0)
return;
foreach (var (name, addedOff) in namesOffsets) {
var loc = location;
var piv = pivot;
var off = offset + addedOff;
loc.Offset(off.ToPoint());
if (piv != Vector2.Zero) {
piv += off;
if (!pivotRelative)
piv -= loc.Location.ToVector2();
}
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = name
};
foreach (var kv in customData)
region.SetData(kv.Key, kv.Value);
atlas.regions.Add(name, region);
}
// we only clear names offsets if the location was valid, otherwise we ignore multiple names for a region
namesOffsets.Clear();
}
} }
} }
@ -128,6 +216,7 @@ namespace MLEM.Data {
/// <summary> /// <summary>
/// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file. /// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file.
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
/// </summary> /// </summary>
/// <param name="content">The content manager to use for loading</param> /// <param name="content">The content manager to use for loading</param>
/// <param name="texturePath">The path to the texture file</param> /// <param name="texturePath">The path to the texture file</param>

View file

@ -13,11 +13,21 @@ TestRegionNegativePivot
loc 0 32 +16 16 loc 0 32 +16 16
piv -32 +46 piv -32 +46
DataTest
loc 0 0 16 16
dat DataPoint1 ThisIsSomeData
dat DataPoint2 3.5
dat DataPoint3 ---
LongTableUp LongTableUp
loc 0 32 64 48 piv 16 48 loc 0 32 64 48
piv 16 48 copy Copy1 16 0
cpy Copy2 32 4
Copy3 from
LongTableUp off 2 4
LongTableRight LongTableDown LongTableLeft LongTableRight LongTableDown LongTableLeft
loc 32 30 64 48 location 32 30 64 48
piv 80 46 piv 80 46
off 32 2 offset 32 2

View file

@ -9,26 +9,51 @@ namespace Tests {
public class TestDataTextureAtlas { public class TestDataTextureAtlas {
[Test] [Test]
public void Test() { public void Test([Values(0, 4)] int regionX, [Values(0, 4)] int regionY) {
using var game = TestGame.Create(); using var game = TestGame.Create();
using var texture = new Texture2D(game.GraphicsDevice, 1, 1); using var texture = new Texture2D(game.GraphicsDevice, 1, 1);
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas"); var region = new TextureRegion(texture, regionX, regionY, 1, 1);
Assert.AreEqual(atlas.Regions.Count(), 8); var atlas = DataTextureAtlas.LoadAtlasData(region, game.RawContent, "Texture.atlas");
Assert.AreEqual(12, atlas.Regions.Count());
// no pivot
var plant = atlas["Plant"];
Assert.AreEqual(plant.Area, new Rectangle(96 + regionX, 0 + regionY, 16, 32));
Assert.AreEqual(plant.PivotPixels, Vector2.Zero);
// no added offset // no added offset
var table = atlas["LongTableUp"]; var table = atlas["LongTableUp"];
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48)); Assert.AreEqual(table.Area, new Rectangle(0 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32)); Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32));
// added offset // added offset
var table2 = atlas["LongTableLeft"]; var table2 = atlas["LongTableDown"];
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48)); Assert.AreEqual(table2.Area, new Rectangle(64 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32)); Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
// negative pivot // negative pivot
var negativePivot = atlas["TestRegionNegativePivot"]; var negativePivot = atlas["TestRegionNegativePivot"];
Assert.AreEqual(negativePivot.Area, new Rectangle(0, 32, 16, 16)); Assert.AreEqual(negativePivot.Area, new Rectangle(0 + regionX, 32 + regionY, 16, 16));
Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32)); Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32));
// cpy (pivot pixels should be identical to LongTableUp because they're region-internal)
var copy1 = atlas["Copy1"];
Assert.AreEqual(copy1.Area, new Rectangle(0 + 16 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(copy1.PivotPixels, new Vector2(16, 48 - 32));
var copy2 = atlas["Copy2"];
Assert.AreEqual(copy2.Area, new Rectangle(0 + 32 + regionX, 32 + 4 + regionY, 64, 48));
Assert.AreEqual(copy2.PivotPixels, new Vector2(16, 48 - 32));
// frm
var copy3 = atlas["Copy3"];
Assert.AreEqual(copy3.Area, new Rectangle(0 + 2 + regionX, 32 + 4 + regionY, 64, 48));
Assert.AreEqual(copy3.PivotPixels, new Vector2(16, 48 - 32));
// data
var data = atlas["DataTest"];
Assert.AreEqual("ThisIsSomeData", data.GetData<string>("DataPoint1"));
Assert.AreEqual("3.5", data.GetData<string>("DataPoint2"));
Assert.AreEqual("---", data.GetData<string>("DataPoint3"));
} }
} }