mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-25 14:08:34 +01:00
Compare commits
6 commits
38214a66a3
...
c3c8b132da
Author | SHA1 | Date | |
---|---|---|---|
c3c8b132da | |||
7d8b14ee8d | |||
ff92a00e1a | |||
740c65a887 | |||
4918a72760 | |||
914b0d9c2d |
4 changed files with 175 additions and 48 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
if (Path.IsPathRooted(info)) {
|
||||
text = File.ReadAllText(info);
|
||||
} else {
|
||||
using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
|
||||
text = reader.ReadToEnd();
|
||||
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();
|
||||
|
||||
// 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());
|
||||
|
||||
// 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
|
||||
};
|
||||
atlas.regions.Add(trimmed, region);
|
||||
// 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;
|
||||
|
||||
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>
|
||||
/// 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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue