diff --git a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs index 7ea3d551..c89c9f50 100644 --- a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs +++ b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs @@ -13,7 +13,20 @@ namespace MonoGame.Extended.Content.Pipeline.TextureAtlases public override TexturePackerFileContent Import(string filename, ContentImporterContext context) { var tpFile = TexturePackerFileReader.Read(filename); - context.AddDependency(tpFile.Meta.Image); + + if (tpFile.Meta.Image != null) + { + context.AddDependency(tpFile.Meta.Image); + } + else if (tpFile.Meta.DataFormat == "monogame-extended") + { + // new format: textures are in the textures array + foreach (var texture in tpFile.Textures) + { + context.AddDependency(texture.FileName); + } + } + return tpFile; } } diff --git a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs index 13e1d227..99b0c16e 100644 --- a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs +++ b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs @@ -3,6 +3,8 @@ // See LICENSE file in the project root for full license information. using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using Microsoft.Xna.Framework.Content.Pipeline.Processors; using MonoGame.Extended.Content.TexturePacker; namespace MonoGame.Extended.Content.Pipeline.TextureAtlases; @@ -12,6 +14,21 @@ public class TexturePackerProcessor : ContentProcessor(input.Meta.Image); + context.BuildAndLoadAsset(externalRef, nameof(TextureProcessor)); + + } + else if (input.Meta.DataFormat == "monogame-extended") + { + foreach (var texture in input.Textures) + { + var externalRef = new ExternalReference(texture.FileName); + context.BuildAndLoadAsset(externalRef, nameof(TextureProcessor)); + } + } return new TexturePackerProcessorResult(input); } } diff --git a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs index 9ee763cf..39264209 100644 --- a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs +++ b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // See LICENSE file in the project root for full license information. +using System; using System.IO; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; @@ -15,20 +16,66 @@ public class TexturePackerWriter : ContentTypeWriter Regions, + [property: JsonPropertyName("textures")] List Textures, [property: JsonPropertyName("meta")] TexturePackerMeta Meta); -public record TexturePackerPoint([property: JsonPropertyName("x")] double X, - [property: JsonPropertyName("y")] double Y); + +public record TexturePackerTexture([property: JsonPropertyName("filename")] string FileName, + [property: JsonPropertyName("format")] string? Format = null, + [property: JsonPropertyName("size")] TexturePackerSize Size = default, + [property: JsonPropertyName("scale")] string? Scale = null, + [property: JsonPropertyName("premultiplied")] bool Premultiplied = false, + [property: JsonPropertyName("frames")] Dictionary? Frames = null +); + +public record TexturePackerTextureFrame([property: JsonPropertyName("frame")] TexturePackerRectangle Frame, + [property: JsonPropertyName("rotated")] int Rotated = 0, + [property: JsonPropertyName("size")] TexturePackerSize? Size = null, + [property: JsonPropertyName("offset")] TexturePackerPoint? Offset = null, + [property: JsonPropertyName("pivot")] TexturePackerPointF? Pivot = null, + [property: JsonPropertyName("scale9")] TexturePackerRectangle? Scale9 = null +); +public record TexturePackerPoint([property: JsonPropertyName("x")] int X, + [property: JsonPropertyName("y")] int Y); +public record TexturePackerPointF([property: JsonPropertyName("x")] double X, + [property: JsonPropertyName("y")] double Y); public record TexturePackerSize([property: JsonPropertyName("w")] int Width, [property: JsonPropertyName("h")] int Height); @@ -22,12 +41,10 @@ public record TexturePackerFrame([property: JsonPropertyName("filename")] string [property: JsonPropertyName("trimmed")] bool Trimmed, [property: JsonPropertyName("spriteSourceSize")] TexturePackerRectangle SpriteSourceSize, [property: JsonPropertyName("sourceSize")] TexturePackerSize SourceSize, - [property: JsonPropertyName("pivot")] TexturePackerPoint PivotPoint); + [property: JsonPropertyName("pivot")] TexturePackerPointF PivotPoint); public record TexturePackerMeta([property: JsonPropertyName("app")] string App, [property: JsonPropertyName("version")] string Version, [property: JsonPropertyName("image")] string Image, - [property: JsonPropertyName("format")] string Format, - [property: JsonPropertyName("size")] TexturePackerSize Size, - [property: JsonPropertyName("scale"), JsonConverter(typeof(FloatStringConverter))] float Scale, + [property: JsonPropertyName("dataformat")] string DataFormat, [property: JsonPropertyName("smartupdate")] string SmartUpdate); diff --git a/source/MonoGame.Extended/Graphics/NinePatch.cs b/source/MonoGame.Extended/Graphics/NinePatch.cs index 7a9e1069..fcdf883b 100644 --- a/source/MonoGame.Extended/Graphics/NinePatch.cs +++ b/source/MonoGame.Extended/Graphics/NinePatch.cs @@ -121,8 +121,8 @@ public class NinePatch _patches = patches; - Size topLeft = patches[NinePatch.TopLeft].Size; - Size bottomRight = patches[NinePatch.BottomRight].Size; + Size topLeft = patches[NinePatch.TopLeft].OriginalSize; + Size bottomRight = patches[NinePatch.BottomRight].OriginalSize; Padding = new Thickness(topLeft.Width, topLeft.Height, bottomRight.Width, bottomRight.Height); Name = name; diff --git a/source/MonoGame.Extended/Graphics/Sprite.cs b/source/MonoGame.Extended/Graphics/Sprite.cs index d21623b8..5334504b 100644 --- a/source/MonoGame.Extended/Graphics/Sprite.cs +++ b/source/MonoGame.Extended/Graphics/Sprite.cs @@ -61,6 +61,11 @@ public class Sprite : IColorable /// public object Tag { get; set; } + /// + /// Gets the size of the sprite. + /// + public Point Size => TextureRegion.OriginalSize; + /// /// Gets or sets the origin of this sprite. /// @@ -79,8 +84,8 @@ public class Sprite : IColorable /// public Vector2 OriginNormalized { - get { return new Vector2(Origin.X / TextureRegion.Width, Origin.Y / TextureRegion.Height); } - set { Origin = new Vector2(value.X * TextureRegion.Width, value.Y * TextureRegion.Height); } + get { return new Vector2(Origin.X / TextureRegion.OriginalSize.Width, Origin.Y / TextureRegion.OriginalSize.Height); } + set { Origin = new Vector2(value.X * TextureRegion.OriginalSize.Width, value.Y * TextureRegion.OriginalSize.Height); } } /// @@ -101,6 +106,11 @@ public class Sprite : IColorable throw new ObjectDisposedException(nameof(value), $"The source {nameof(Texture2D)} of the {nameof(TextureRegion)} was disposed prior to setting this property."); } _textureRegion = value; + + if (value.OriginNormalized.HasValue) + { + OriginNormalized = value.OriginNormalized.Value; + } } } @@ -145,7 +155,7 @@ public class Sprite : IColorable Color = Color.White; IsVisible = true; Effect = SpriteEffects.None; - OriginNormalized = Vector2.Zero; + OriginNormalized = textureRegion.OriginNormalized ?? Vector2.Zero; Depth = 0.0f; } @@ -184,7 +194,7 @@ public class Sprite : IColorable public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale) { var min = -Origin; - var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height); + var max = min + new Vector2(TextureRegion.OriginalSize.Width, TextureRegion.OriginalSize.Height); var offset = position; if (scale != Vector2.One) diff --git a/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs b/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs index 63b7e225..2f685210 100644 --- a/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs +++ b/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs @@ -32,21 +32,17 @@ public static class SpriteBatchExtensions for (int i = 0; i < sourcePatches.Length; i++) { - Rectangle source = sourcePatches[i].Bounds; - Rectangle destination = _patchCache[i]; + Texture2DRegion sourceRegion = sourcePatches[i]; + Rectangle destinationRect = _patchCache[i]; if (clippingRectangle.HasValue) { - source = ClipSourceRectangle(source, destination, clippingRectangle.Value); - destination = ClipDestinationRectangle(destination, clippingRectangle.Value); - Draw(spriteBatch, sourcePatches[i].Texture, source, destination, color, clippingRectangle); + sourceRegion = ClipSourceRegion(sourceRegion, destinationRect, clippingRectangle.Value); + destinationRect = ClipDestinationRectangle(destinationRect, clippingRectangle.Value); } - else + if (sourceRegion != null && !destinationRect.IsEmpty) { - if (destination.Width > 0 && destination.Height > 0) - { - spriteBatch.Draw(sourcePatches[i].Texture, destination, source, color); - } + Draw(spriteBatch, sourceRegion, destinationRect, color); } } } @@ -104,9 +100,17 @@ public static class SpriteBatchExtensions if (sprite.IsVisible) { - var texture = sprite.TextureRegion.Texture; - var sourceRectangle = sprite.TextureRegion.Bounds; - spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color * sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth); + Draw( + spriteBatch, + sprite.TextureRegion, + position, + sprite.Color * sprite.Alpha, + rotation, + sprite.Origin, + scale, + sprite.Effect, + sprite.Depth + ); } } #endregion -------------------------Sprite----------------------------- @@ -164,29 +168,49 @@ public static class SpriteBatchExtensions /// The layer depth. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, - float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null) + float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null) { var sourceRectangle = textureRegion.Bounds; + var offset = (textureRegion.Offset - origin) * scale; + Vector2 drawScale = scale; + float drawRotation = rotation; + + // Handle rotated texture regions + if (textureRegion.IsRotated) + { + offset.Y += textureRegion.Width * scale.Y; + + // Swap scale axes and adjust rotation for rotated regions + drawScale = new Vector2(scale.Y, scale.X); + drawRotation = rotation - (float)Math.PI / 2; + } + + var drawPosition = position + RotateVector(offset, rotation); if (clippingRectangle.HasValue) { - var x = (int)(position.X - origin.X); - var y = (int)(position.Y - origin.Y); - var width = (int)(textureRegion.Width * scale.X); - var height = (int)(textureRegion.Height * scale.Y); + var x = (int)drawPosition.X; + var y = (int)drawPosition.Y; + var width = (int)(textureRegion.Width * drawScale.X); + var height = (int)(textureRegion.Height * drawScale.Y); + if (textureRegion.IsRotated) + { + (width, height) = (height, width); + y -= height; + } var destinationRectangle = new Rectangle(x, y, width, height); - if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle)) + if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle, textureRegion.IsRotated)) { // Clipped rectangle is empty, nothing to draw return; } - - position.X = destinationRectangle.X + origin.X; - position.Y = destinationRectangle.Y + origin.Y; + drawPosition.X = destinationRectangle.X; + drawPosition.Y = textureRegion.IsRotated ? destinationRectangle.Y + destinationRectangle.Height + : destinationRectangle.Y; } - spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth); + spriteBatch.Draw(textureRegion.Texture, drawPosition, sourceRectangle, color, drawRotation, Vector2.Zero, drawScale, effects, layerDepth); } /// @@ -199,7 +223,9 @@ public static class SpriteBatchExtensions /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) { - Draw(spriteBatch, textureRegion.Texture, textureRegion.Bounds, destinationRectangle, color, clippingRectangle); + float scaleX = (float)destinationRectangle.Width / textureRegion.OriginalSize.Width; + float scaleY = (float)destinationRectangle.Height / textureRegion.OriginalSize.Height; + Draw(spriteBatch, textureRegion, new Vector2(destinationRectangle.X, destinationRectangle.Y), color, 0, Vector2.Zero, new Vector2(scaleX, scaleY), SpriteEffects.None, 0, clippingRectangle); } #endregion -------------------------TextureRegion----------------------------- @@ -227,7 +253,7 @@ public static class SpriteBatchExtensions _patchCache[NinePatch.BottomMiddle] = new Rectangle(left, bottom, midWidth, bottomPadding); _patchCache[NinePatch.BottomRight] = new Rectangle(right, bottom, rightPadding, bottomPadding); } - private static bool ClipRectangles(ref Rectangle sourceRectangle, ref Rectangle destinationRectangle, Rectangle? clippingRectangle) + private static bool ClipRectangles(ref Rectangle sourceRectangle, ref Rectangle destinationRectangle, Rectangle? clippingRectangle, bool rotatedSource = false) { if (!clippingRectangle.HasValue) return true; @@ -238,21 +264,35 @@ public static class SpriteBatchExtensions if (destinationRectangle == Rectangle.Empty) return false; // Clipped rectangle is empty, nothing to draw - var scaleX = (float)sourceRectangle.Width / originalDestination.Width; - var scaleY = (float)sourceRectangle.Height / originalDestination.Height; - int leftDiff = destinationRectangle.Left - originalDestination.Left; int topDiff = destinationRectangle.Top - originalDestination.Top; + int bottomDiff = originalDestination.Bottom - destinationRectangle.Bottom; - sourceRectangle.X += (int)(leftDiff * scaleX); - sourceRectangle.Y += (int)(topDiff * scaleY); - sourceRectangle.Width = (int)(destinationRectangle.Width * scaleX); - sourceRectangle.Height = (int)(destinationRectangle.Height * scaleY); + if (rotatedSource) + { + var scaleX = (float)sourceRectangle.Height / originalDestination.Width; + var scaleY = (float)sourceRectangle.Width / originalDestination.Height; + + sourceRectangle.X += (int)(bottomDiff * scaleY); + sourceRectangle.Y += (int)(leftDiff * scaleX); + sourceRectangle.Width = (int)(destinationRectangle.Height * scaleY); + sourceRectangle.Height = (int)(destinationRectangle.Width * scaleX); + } + else + { + var scaleX = (float)sourceRectangle.Width / originalDestination.Width; + var scaleY = (float)sourceRectangle.Height / originalDestination.Height; + + sourceRectangle.X += (int)(leftDiff * scaleX); + sourceRectangle.Y += (int)(topDiff * scaleY); + sourceRectangle.Width = (int)(destinationRectangle.Width * scaleX); + sourceRectangle.Height = (int)(destinationRectangle.Height * scaleY); + } return true; } - private static Rectangle ClipSourceRectangle(Rectangle sourceRectangle, Rectangle destinationRectangle, Rectangle clippingRectangle) + private static Texture2DRegion ClipSourceRegion(Texture2DRegion sourceRegion, Rectangle destinationRectangle, Rectangle clippingRectangle) { var left = (float)(clippingRectangle.Left - destinationRectangle.Left); var right = (float)(destinationRectangle.Right - clippingRectangle.Right); @@ -263,23 +303,31 @@ public static class SpriteBatchExtensions var w = (right > 0 ? right : 0) + x; var h = (bottom > 0 ? bottom : 0) + y; - var scaleX = (float)destinationRectangle.Width / sourceRectangle.Width; - var scaleY = (float)destinationRectangle.Height / sourceRectangle.Height; + var scaleX = (float)destinationRectangle.Width / sourceRegion.OriginalSize.Width; + var scaleY = (float)destinationRectangle.Height / sourceRegion.OriginalSize.Height; x /= scaleX; y /= scaleY; w /= scaleX; h /= scaleY; - return new Rectangle((int)(sourceRectangle.X + x), (int)(sourceRectangle.Y + y), (int)(sourceRectangle.Width - w), (int)(sourceRectangle.Height - h)); + return sourceRegion.GetSubregion((int)x, (int)y, (int)(sourceRegion.OriginalSize.Width - w), (int)(sourceRegion.OriginalSize.Height - h)); } private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle) { - var left = clippingRectangle.Left < destinationRectangle.Left ? destinationRectangle.Left : clippingRectangle.Left; - var top = clippingRectangle.Top < destinationRectangle.Top ? destinationRectangle.Top : clippingRectangle.Top; - var bottom = clippingRectangle.Bottom < destinationRectangle.Bottom ? clippingRectangle.Bottom : destinationRectangle.Bottom; - var right = clippingRectangle.Right < destinationRectangle.Right ? clippingRectangle.Right : destinationRectangle.Right; - return new Rectangle(left, top, right - left, bottom - top); + return destinationRectangle.Clip(clippingRectangle); + } + + // Rotates a 2D vector by the specified angle in radians. + private static Vector2 RotateVector(Vector2 original, double rotation) + { + double cosTheta = Math.Cos(rotation); + double sinTheta = Math.Sin(rotation); + + double rotatedX = original.X * cosTheta - original.Y * sinTheta; + double rotatedY = original.X * sinTheta + original.Y * cosTheta; + + return new Vector2((float)rotatedX, (float)rotatedY); } #endregion -------------------------Utilities----------------------------- diff --git a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs index daf766ba..2a19b5e9 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs @@ -174,6 +174,23 @@ public class Texture2DAtlas : IEnumerable return region; } + /// + /// Creates a new texture region with the specified name and adds it to this atlas. + /// + /// The bounds of the region. + /// The name of the texture region. + /// A value indicating whether this texture region is rotated 90 degrees clockwise in the atlas. + /// The created texture region. + /// + /// Thrown if a region with the same name as the parameter already exists in this atlas. + /// + public Texture2DRegion CreateRegion(Rectangle bounds, bool isRotated, Size originalSize, Vector2 trimOffset, Vector2? originNormalized, string name) + { + Texture2DRegion region = new Texture2DRegion(Texture, bounds.X, bounds.Y, bounds.Width, bounds.Height, isRotated, originalSize, trimOffset, originNormalized, name); + AddRegion(region); + return region; + } + /// /// Determines whether the atlas contains a region with the specified name. /// diff --git a/source/MonoGame.Extended/Graphics/Texture2DRegion.Extensions.cs b/source/MonoGame.Extended/Graphics/Texture2DRegion.Extensions.cs index a0fbce90..d10d4885 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DRegion.Extensions.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DRegion.Extensions.cs @@ -65,8 +65,8 @@ public static class Texture2DRegionExtensions /// Gets a subregion of the specified texture region using the provided name, coordinates, and dimensions. /// /// The texture region to get the subregion from. - /// The top-left x-coordinate of the subregion within the texture region. - /// The top-left y-coordinate of the subregion within the texture region. + /// The top-left x-coordinate of the subregion within the original sprite. + /// The top-left y-coordinate of the subregion within the original sprite. /// The width, in pixels, of the subregion. /// The height, in pixels, of the subregion. /// The name of the new subregion. @@ -86,8 +86,47 @@ public static class Texture2DRegionExtensions name = $"{textureRegion.Texture.Name}({x}, {y}, {width}, {height})"; } - Rectangle region = textureRegion.Bounds.GetRelativeRectangle(x, y, width, height); - return new Texture2DRegion(textureRegion.Texture, region, name); + // The requested subregion is in original sprite coordinates + Rectangle requestedRegion = new Rectangle(x, y, width, height); + + // Calculate the bounds of the trimmed region in original sprite coordinates + Rectangle trimmedBounds = new Rectangle( + (int)textureRegion.Offset.X, + (int)textureRegion.Offset.Y, + textureRegion.IsRotated ? textureRegion.Height : textureRegion.Width, + textureRegion.IsRotated ? textureRegion.Width : textureRegion.Height); + + // Find intersection between requested subregion and the actual trimmed region + Rectangle intersection = Rectangle.Intersect(requestedRegion, trimmedBounds); + + if (intersection.IsEmpty) + { + return null; + } + + // The subregion offset must include the existing offset of the input region + Vector2 subregionOffset = new Vector2(Math.Max(0, textureRegion.Offset.X - x), + Math.Max(0, textureRegion.Offset.Y - y)); + + if (textureRegion.IsRotated) + { + // Calculate the actual texture coordinates + int textureX = textureRegion.X + (textureRegion.Width + (int)textureRegion.Offset.Y - intersection.Bottom); + int textureY = textureRegion.Y + (intersection.X - (int)textureRegion.Offset.X); + + return new Texture2DRegion(textureRegion.Texture, textureX, textureY, intersection.Height, intersection.Width, + textureRegion.IsRotated, new Size(width, height), subregionOffset, textureRegion.OriginNormalized, name); + } + else + { + // Calculate the actual texture coordinates + int textureX = textureRegion.X + (intersection.X - (int)textureRegion.Offset.X); + int textureY = textureRegion.Y + (intersection.Y - (int)textureRegion.Offset.Y); + + return new Texture2DRegion(textureRegion.Texture, textureX, textureY, intersection.Width, intersection.Height, + textureRegion.IsRotated, new Size(width, height), subregionOffset, textureRegion.OriginNormalized, name); + + } } /// @@ -139,10 +178,11 @@ public static class Texture2DRegionExtensions { Texture2DRegion[] patches = new Texture2DRegion[9]; - int middleWidth = textureRegion.Width - leftPadding - rightPadding; - int middleHeight = textureRegion.Height - topPadding - bottomPadding; - int rightX = textureRegion.Width - rightPadding; - int bottomY = textureRegion.Height - bottomPadding; + // Use original sprite dimensions for calculations + int middleWidth = textureRegion.OriginalSize.Width - leftPadding - rightPadding; + int middleHeight = textureRegion.OriginalSize.Height - topPadding - bottomPadding; + int rightX = textureRegion.OriginalSize.Width - rightPadding; + int bottomY = textureRegion.OriginalSize.Height - bottomPadding; patches[NinePatch.TopLeft] = textureRegion.GetSubregion(0, 0, leftPadding, topPadding); patches[NinePatch.TopMiddle] = textureRegion.GetSubregion(leftPadding, 0, middleWidth, topPadding); diff --git a/source/MonoGame.Extended/Graphics/Texture2DRegion.cs b/source/MonoGame.Extended/Graphics/Texture2DRegion.cs index aada7d0a..60ab2c52 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DRegion.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DRegion.cs @@ -78,6 +78,26 @@ public class Texture2DRegion /// public float LeftUV { get; } + /// + /// Gets a value indicating whether this texture region is rotated 90 degrees clockwise in the atlas. + /// + public bool IsRotated { get; } + + /// + /// Gets the original size of the texture region before trimming. + /// + public Size OriginalSize { get; } + + /// + /// Gets the offset between the top-left corner of the original sprite and the top-left corner of the trimmed sprite. + /// + public Vector2 Offset { get; } + + /// + /// Gets the normalized origin point of the texture region, or null if no origin is specified. + /// + public Vector2? OriginNormalized { get; } + /// /// Initializes a new instance of the class representing the entire texture. /// @@ -170,6 +190,29 @@ public class Texture2DRegion /// Thrown if has been disposed prior. /// public Texture2DRegion(Texture2D texture,int x, int y, int width, int height, string name) + : this(texture, x, y, width, height, false, new Size(width, height), Vector2.Zero, null, name) { } + + /// + /// Initializes a new instance of the class with the specified region of the texture, + /// original size, offset, origin, and name. + /// + /// The texture to create the region from. + /// The top-left x-coordinate of the region within the texture. + /// The top-left y-coordinate of the region within the texture. + /// The width, in pixels, of the region. + /// The height, in pixels, of the region. + /// A value indicating whether this texture region is rotated 90 degrees clockwise in the atlas. + /// The original size of the texture region before trimming. + /// The offset between the top-left corner of the original sprite and the top-left corner of the trimmed sprite. + /// The origin point of the texture region, or null if no origin is specified. + /// The name of the texture region. + /// + /// Thrown if is . + /// + /// + /// Thrown if has been disposed prior. + /// + public Texture2DRegion(Texture2D texture, int x, int y, int width, int height, bool isRotated, Size originalSize, Vector2 offset, Vector2? originNormalized, string name) { ArgumentNullException.ThrowIfNull(texture); if (texture.IsDisposed) @@ -188,6 +231,10 @@ public class Texture2DRegion Y = y; Width = width; Height = height; + IsRotated = isRotated; + OriginalSize = originalSize; + Offset = offset; + OriginNormalized = originNormalized; Bounds = new Rectangle(x, y, width, height); Size = new Size(width, height); TopUV = Bounds.Top / (float)texture.Height; @@ -210,6 +257,9 @@ public class Texture2DRegion Height = height; Bounds = new Rectangle(x, y, width, height); Size = new Size(width, height); + OriginalSize = Size; + Offset = Vector2.Zero; + OriginNormalized = null; TopUV = Bounds.Top / 1.0f; RightUV = Bounds.Right / 1.0f; BottomUV = Bounds.Bottom / 1.0f; diff --git a/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj b/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj index b70b1f62..a0d88934 100644 --- a/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj +++ b/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-atlas.json b/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-atlas.json new file mode 100644 index 00000000..cf13b67e --- /dev/null +++ b/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-atlas.json @@ -0,0 +1,120 @@ +{ + "textures": [ + { + "filename": "test-atlas-texture.png", + "format": "RGBA8888", + "size": {"w":917,"h":462}, + "frames": { + "0001.png":{ + "frame": {"x":164,"y":1,"w":158,"h":316}, + "size": {"w":187,"h":324}, + "offset": {"x":15,"y":3}, + "pivot": {"x":0.5,"y":1} + }, + "0002.png":{ + "frame": {"x":164,"y":1,"w":158,"h":316}, + "size": {"w":187,"h":324}, + "offset": {"x":15,"y":3}, + "pivot": {"x":0.5,"y":1} + }, + "0003.png":{ + "frame": {"x":622,"y":155,"w":168,"h":303}, + "size": {"w":187,"h":324}, + "offset": {"x":0,"y":8}, + "pivot": {"x":0.5,"y":1} + }, + "0004.png":{ + "frame": {"x":622,"y":155,"w":168,"h":303}, + "size": {"w":187,"h":324}, + "offset": {"x":0,"y":8}, + "pivot": {"x":0.5,"y":1} + }, + "0005.png":{ + "frame": {"x":609,"y":1,"w":307,"h":152}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":26,"y":2}, + "pivot": {"x":0.5,"y":1} + }, + "0006.png":{ + "frame": {"x":609,"y":1,"w":307,"h":152}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":26,"y":2}, + "pivot": {"x":0.5,"y":1} + }, + "0007.png":{ + "frame": {"x":314,"y":319,"w":306,"h":141}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":42,"y":1}, + "pivot": {"x":0.5,"y":1} + }, + "0008.png":{ + "frame": {"x":314,"y":319,"w":306,"h":141}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":42,"y":1}, + "pivot": {"x":0.5,"y":1} + }, + "0009.png":{ + "frame": {"x":1,"y":322,"w":311,"h":139}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":33,"y":3}, + "pivot": {"x":0.5,"y":1} + }, + "0010.png":{ + "frame": {"x":1,"y":322,"w":311,"h":139}, + "rotated": 90, + "size": {"w":187,"h":324}, + "offset": {"x":33,"y":3}, + "pivot": {"x":0.5,"y":1} + }, + "0011.png":{ + "frame": {"x":472,"y":1,"w":135,"h":311}, + "size": {"w":187,"h":324}, + "offset": {"x":29,"y":8}, + "pivot": {"x":0.5,"y":1} + }, + "0012.png":{ + "frame": {"x":472,"y":1,"w":135,"h":311}, + "size": {"w":187,"h":324}, + "offset": {"x":29,"y":8}, + "pivot": {"x":0.5,"y":1} + }, + "0013.png":{ + "frame": {"x":324,"y":1,"w":146,"h":314}, + "size": {"w":187,"h":324}, + "offset": {"x":32,"y":2}, + "pivot": {"x":0.5,"y":1} + }, + "0014.png":{ + "frame": {"x":324,"y":1,"w":146,"h":314}, + "size": {"w":187,"h":324}, + "offset": {"x":32,"y":2}, + "pivot": {"x":0.5,"y":1} + }, + "0015.png":{ + "frame": {"x":1,"y":1,"w":161,"h":319}, + "size": {"w":187,"h":324}, + "offset": {"x":22,"y":1}, + "pivot": {"x":0.5,"y":1} + }, + "0016.png":{ + "frame": {"x":1,"y":1,"w":161,"h":319}, + "size": {"w":187,"h":324}, + "offset": {"x":22,"y":1}, + "pivot": {"x":0.5,"y":1} + } + } + } + ], + + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "dataformat": "monogame-extended", + "version": "1.2", + "smartupdate": "$TexturePacker:SmartUpdate:682bde45425e0dc70518ec1f7428e5ba:7979479a5ba52a9c36728fb50f2150eb:a2acd4b7aba6ca32e8c17414ae3e522b$" + } +} diff --git a/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs b/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs index c73b4754..7675d4ec 100644 --- a/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs +++ b/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs @@ -15,6 +15,26 @@ namespace MonoGame.Extended.Content.Pipeline.Tests var data = importer.Import(filePath, Substitute.For()); Assert.NotNull(data); + + // Check meta.image contains image name + Assert.Equal("test-tileset.png", data.Meta.Image); + + // Check regions count and textures + Assert.Equal(9, data.Regions.Count); + Assert.Null(data.Textures); // only used by new MonoGame.Extended JSON format + + // Check first region + var firstRegion = data.Regions[0]; + Assert.Equal("1.png", firstRegion.FileName); + Assert.Equal(2, firstRegion.Frame.X); + Assert.Equal(2, firstRegion.Frame.Y); + Assert.Equal(32, firstRegion.Frame.Width); + Assert.Equal(32, firstRegion.Frame.Height); + Assert.Equal(0.5, firstRegion.PivotPoint.X); + Assert.Equal(0.5, firstRegion.PivotPoint.Y); + Assert.False(firstRegion.Rotated); + Assert.Equal(32, firstRegion.SourceSize.Width); + Assert.Equal(32, firstRegion.SourceSize.Height); } [Fact] @@ -28,5 +48,48 @@ namespace MonoGame.Extended.Content.Pipeline.Tests Assert.NotNull(output); } + + [Fact] + public void TexturePackerJsonImporter_Import_NewFormat_Test() + { + var filePath = PathExtensions.GetApplicationFullPath(@"TestData/test-atlas.json"); + var importer = new TexturePackerJsonImporter(); + var data = importer.Import(filePath, Substitute.For()); + + // Regions must be null (only used by old generic json format) + Assert.Null(data.Regions); + + // Textures contains 1 texture + Assert.NotNull(data.Textures); + Assert.Single(data.Textures); + + var texture = data.Textures[0]; + Assert.Equal("test-atlas-texture.png", texture.FileName); + + // The first texture contains frames + Assert.NotNull(texture.Frames); + Assert.Contains("0001.png", texture.Frames.Keys); + + // Check properties of the first frame ("0001.png") + var frame = texture.Frames["0001.png"]; + Assert.Equal(164, frame.Frame.X); + Assert.Equal(1, frame.Frame.Y); + Assert.Equal(158, frame.Frame.Width); + Assert.Equal(316, frame.Frame.Height); + + Assert.NotNull(frame.Size); + Assert.Equal(187, frame.Size.Width); + Assert.Equal(324, frame.Size.Height); + + Assert.NotNull(frame.Offset); + Assert.Equal(15, frame.Offset.X); + Assert.Equal(3, frame.Offset.Y); + + Assert.Equal(0, frame.Rotated); + + Assert.NotNull(frame.Pivot); + Assert.Equal(0.5, frame.Pivot.X); + Assert.Equal(1.0, frame.Pivot.Y); + } } }