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
### MLEM.Data
Additions
- Added data, from, and copy instructions to DataTextureAtlas
Improvements
- Allow data texture atlas pivots and offsets to be negative
- Made RuntimeTexturePacker restore texture region name and pivot when packing

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -5,6 +6,8 @@ using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Misc;
using MLEM.Textures;
#if FNA
using MLEM.Extensions;
@ -18,15 +21,20 @@ namespace MLEM.Data {
/// </para>
/// <para>
/// 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.
/// 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.
/// 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.
/// 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.
/// 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.
/// <list type="bullet">
/// <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>
/// <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>
/// <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>
/// LongTableRight
/// LongTableRight LongTableUp
/// loc 32 30 64 48
/// piv 80 46
/// </code>
@ -69,6 +77,7 @@ namespace MLEM.Data {
/// <summary>
/// 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>
/// <param name="texture">The texture to use for this data texture atlas</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) {
var info = Path.Combine(content.RootDirectory, infoPath);
string text;
try {
if (Path.IsPathRooted(info)) {
text = File.ReadAllText(info);
} else {
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 words = Regex.Split(text, @"\s+");
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
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.+-]+))?")) {
// offset
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture));
var namesOffsets = new List<(string, Vector2)>();
var customData = new Dictionary<string, string>();
var location = Rectangle.Empty;
var pivot = Vector2.Zero;
var offset = Vector2.Zero;
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();
// we're starting a new region (or adding another name for a new region), so clear old data
namesOffsets.Add((word.Trim(), Vector2.Zero));
customData.Clear();
location = Rectangle.Empty;
pivot = Vector2.Zero;
offset = Vector2.Zero;
break;
}
} catch (Exception e) {
throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e);
}
}
// add the last region that was started on
AddCurrentRegions();
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;
// location
var loc = new Rectangle(
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
loc.Offset(off.ToPoint());
if (piv != Vector2.Zero) {
piv += off;
if (!pivotRelative)
piv -= loc.Location.ToVector2();
}
// pivot
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
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
Name = name
};
atlas.regions.Add(trimmed, region);
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();
}
return atlas;
}
}
@ -128,6 +216,7 @@ namespace MLEM.Data {
/// <summary>
/// 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>
/// <param name="content">The content manager to use for loading</param>
/// <param name="texturePath">The path to the texture file</param>

View file

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

View file

@ -9,26 +9,51 @@ namespace Tests {
public class TestDataTextureAtlas {
[Test]
public void Test() {
public void Test([Values(0, 4)] int regionX, [Values(0, 4)] int regionY) {
using var game = TestGame.Create();
using var texture = new Texture2D(game.GraphicsDevice, 1, 1);
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas");
Assert.AreEqual(atlas.Regions.Count(), 8);
var region = new TextureRegion(texture, regionX, regionY, 1, 1);
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
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));
// added offset
var table2 = atlas["LongTableLeft"];
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48));
var table2 = atlas["LongTableDown"];
Assert.AreEqual(table2.Area, new Rectangle(64 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
// negative pivot
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));
// 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"));
}
}