Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ protected override void UpdateComponentsFromValue(Thickness? value)
}

/// <inheritdoc/>
protected override Thickness? UpateValueFromFloat(float value)
protected override Thickness? UpdateValueFromFloat(float value)
{
return Thickness.UniformCuboid(value);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Stride.UI;assembly=Stride.UI"
xmlns:views="clr-namespace:Stride.Assets.Presentation.AssetEditors.UIEditor.Views"
xmlns:tp="clr-namespace:Stride.Core.Assets.Editor.View.TemplateProviders;assembly=Stride.Core.Assets.Editor"
xmlns:sd="http://schemas.stride3d.net/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Stride.Core.Assets.Editor;component/View/DefaultPropertyTemplateProviders.xaml"/>
<ResourceDictionary Source="ImageDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>

<tp:TypeNameMatchTemplateProvider x:Key="RotationTemplateProvider" Type="{x:Type s:Single}" PropertyName="Rotation" sd:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate>
<sd:AngleEditor Value="{Binding NodeValue}" />
</DataTemplate>
</tp:TypeNameMatchTemplateProvider>

<sd:TypeMatchTemplateProvider x:Key="StripDefinitionTemplateProvider" Type="{x:Type ui:StripDefinition}" sd:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate>
<UniformGrid Rows="1">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Presentation.Quantum.ViewModels;

namespace Stride.Core.Assets.Editor.View.TemplateProviders
{
/// <summary>
/// A template provider that matches nodes based on both their type and name.
/// </summary>
public class TypeNameMatchTemplateProvider : TypeMatchTemplateProvider
{
/// <inheritdoc/>
public override string Name => $"{base.Name}_{PropertyName}";

/// <summary>
/// Gets or sets the name of the property to match.
/// </summary>
public string PropertyName { get; set; }

public override bool MatchNode(NodeViewModel node)
{
return base.MatchNode(node) && node.Name == PropertyName;
}
}
}
188 changes: 188 additions & 0 deletions sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.

using Stride.Core.Mathematics;
using Stride.Graphics;
using Stride.UI.Controls;
using Xunit;

namespace Stride.UI.Tests.Layering
{
/// <summary>
/// Tests for the <see cref="ImageElement.Rotation"/> property.
/// </summary>
[System.ComponentModel.Description("Tests for ImageElement rotation functionality")]
public class ImageElementRotationTests
{
[Fact]
[System.ComponentModel.Description("Test that the default rotation value is 0")]
public void TestDefaultRotation()
{
var image = new ImageElement();
Assert.Equal(0f, image.Rotation);
}

[Fact]
[System.ComponentModel.Description("Test that the default LocalMatrix is Identity when rotation is 0")]
public void TestDefaultLocalMatrix()
{
var image = new ImageElement();
Assert.Equal(Matrix.Identity, image.LocalMatrix);
}

[Fact]
[System.ComponentModel.Description("Test setting rotation to a positive value")]
public void TestSetPositiveRotation()
{
var image = new ImageElement();
var angle = MathUtil.PiOverFour; // 45 degrees

image.Rotation = angle;

Assert.Equal(angle, image.Rotation);
}

[Fact]
[System.ComponentModel.Description("Test setting rotation to a negative value (counter-clockwise)")]
public void TestSetNegativeRotation()
{
var image = new ImageElement();
var angle = -MathUtil.PiOverFour; // -45 degrees

image.Rotation = angle;

Assert.Equal(angle, image.Rotation);
}

[Fact]
[System.ComponentModel.Description("Test that LocalMatrix is updated when rotation changes")]
public void TestLocalMatrixUpdatesOnRotationChange()
{
var image = new ImageElement();
var angle = MathUtil.PiOverTwo; // 90 degrees

image.Rotation = angle;

var expectedMatrix = Matrix.RotationZ(angle);
AssertMatrixEqual(expectedMatrix, image.LocalMatrix);
}

[Fact]
[System.ComponentModel.Description("Test that setting rotation to 0 resets LocalMatrix to Identity")]
public void TestRotationZeroResetsToIdentity()
{
var image = new ImageElement();

// First set a non-zero rotation
image.Rotation = MathUtil.PiOverFour;
Assert.NotEqual(Matrix.Identity, image.LocalMatrix);

// Then reset to zero
image.Rotation = 0f;
Assert.Equal(Matrix.Identity, image.LocalMatrix);
}

[Fact]
[System.ComponentModel.Description("Test that very small rotation values (near zero) set LocalMatrix to Identity")]
public void TestVerySmallRotationSetsIdentity()
{
var image = new ImageElement();

// Set a value smaller than float.Epsilon
image.Rotation = float.Epsilon / 2f;

// Should be treated as zero
Assert.Equal(Matrix.Identity, image.LocalMatrix);
}

[Fact]
[System.ComponentModel.Description("Test multiple rotation changes")]
public void TestMultipleRotationChanges()
{
var image = new ImageElement();

// First rotation
image.Rotation = MathUtil.PiOverFour;
AssertMatrixEqual(Matrix.RotationZ(MathUtil.PiOverFour), image.LocalMatrix);

// Second rotation
image.Rotation = MathUtil.PiOverTwo;
AssertMatrixEqual(Matrix.RotationZ(MathUtil.PiOverTwo), image.LocalMatrix);

// Third rotation (negative)
image.Rotation = -MathUtil.PiOverFour;
AssertMatrixEqual(Matrix.RotationZ(-MathUtil.PiOverFour), image.LocalMatrix);
}

[Fact]
[System.ComponentModel.Description("Test that setting the same rotation value doesn't trigger unnecessary updates")]
public void TestSetSameRotationValue()
{
var image = new ImageElement();
var angle = MathUtil.PiOverFour;

image.Rotation = angle;
var firstMatrix = image.LocalMatrix;

// Set the same value again
image.Rotation = angle;
var secondMatrix = image.LocalMatrix;

// Matrix should be the same
Assert.Equal(firstMatrix, secondMatrix);
}

[Fact]
[System.ComponentModel.Description("Test that rotation doesn't affect the image size or measurement")]
public void TestRotationDoesNotAffectMeasurement()
{
var sprite = new Sprite()
{
Region = new Rectangle(0, 0, 100, 50)
};
var image = new ImageElement()
{
Source = (Rendering.Sprites.SpriteFromTexture)sprite,
StretchType = StretchType.None
};

// Measure without rotation
image.Measure(new Vector3(200, 200, 0));
var sizeWithoutRotation = image.DesiredSizeWithMargins;

// Apply rotation and measure again
image.Rotation = MathUtil.PiOverFour;
image.Measure(new Vector3(200, 200, 0));
var sizeWithRotation = image.DesiredSizeWithMargins;

// Rotation should not change the measured size
Assert.Equal(sizeWithoutRotation, sizeWithRotation);
}

/// <summary>
/// Helper method to assert that two matrices are approximately equal within a tolerance.
/// </summary>
private static void AssertMatrixEqual(Matrix expected, Matrix actual, int precision = 5)
{
Assert.Equal(expected.M11, actual.M11, precision);
Assert.Equal(expected.M12, actual.M12, precision);
Assert.Equal(expected.M13, actual.M13, precision);
Assert.Equal(expected.M14, actual.M14, precision);

Assert.Equal(expected.M21, actual.M21, precision);
Assert.Equal(expected.M22, actual.M22, precision);
Assert.Equal(expected.M23, actual.M23, precision);
Assert.Equal(expected.M24, actual.M24, precision);

Assert.Equal(expected.M31, actual.M31, precision);
Assert.Equal(expected.M32, actual.M32, precision);
Assert.Equal(expected.M33, actual.M33, precision);
Assert.Equal(expected.M34, actual.M34, precision);

Assert.Equal(expected.M41, actual.M41, precision);
Assert.Equal(expected.M42, actual.M42, precision);
Assert.Equal(expected.M43, actual.M43, precision);
Assert.Equal(expected.M44, actual.M44, precision);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public void TestBasicInvalidations()
UIElementLayeringTests.TestNoInvalidation(this, () => source.Region = new Rectangle(8, 9, 3, 4)); // if the size of the region does not change we avoid re-measuring
UIElementLayeringTests.TestNoInvalidation(this, () => source.Orientation = ImageOrientation.Rotated90); // no changes
UIElementLayeringTests.TestNoInvalidation(this, () => source.Borders = Vector4.One); // no changes
UIElementLayeringTests.TestNoInvalidation(this, () => Rotation = MathUtil.PiOverFour); // rotation does not affect layout/measurement

// ReSharper restore ImplicitlyCapturedClosure
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="Layering\GridTests.cs" />
<Compile Include="Layering\ImageButtonTests.cs" />
<Compile Include="Layering\ImageElementTests.cs" />
<Compile Include="Layering\ImageElementRotationTests.cs" />
<Compile Include="Layering\MeasureArrangeValidator.cs" />
<Compile Include="Layering\MeasureReflector.cs" />
<Compile Include="Layering\MeasureValidator.cs" />
Expand Down
38 changes: 37 additions & 1 deletion sources/engine/Stride.UI/Controls/ImageElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class ImageElement : UIElement
[DefaultValue(null)]
public ISpriteProvider Source
{
get { return source;}
get { return source; }
set
{
if (source == value)
Expand Down Expand Up @@ -86,6 +86,27 @@ public StretchDirection StretchDirection
}
}

/// <summary>
/// Gets or sets the rotation angle in radians (clockwise around Z-axis).
/// </summary>
/// <remarks>The rotation is applied around the center of the image. Positive values rotate clockwise.</remarks>
/// <userdoc>The rotation angle in radians. Positive values rotate clockwise.</userdoc>
[DataMember]
[Display(category: LayoutCategory)]
[DefaultValue(0f)]
public float Rotation
{
get { return field; }
set
{
if (Math.Abs(field - value) < float.Epsilon)
return;

field = value;
UpdateLocalMatrix();
}
}

protected override Vector3 ArrangeOverride(Vector3 finalSizeWithoutMargins)
{
return ImageSizeHelper.CalculateImageSizeFromAvailable(sprite, finalSizeWithoutMargins, StretchType, StretchDirection, false);
Expand Down Expand Up @@ -125,5 +146,20 @@ private void OnSpriteChanged(Sprite currentSprite)
sprite.BorderChanged += InvalidateMeasure;
}
}

/// <summary>
/// Updates the local transformation matrix based on the current rotation angle.
/// </summary>
private void UpdateLocalMatrix()
{
if (Math.Abs(Rotation) < float.Epsilon)
{
LocalMatrix = Matrix.Identity;
}
else
{
LocalMatrix = Matrix.RotationZ(Rotation);
}
}
Comment on lines +155 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this one however, I think the bot is wrong in supposing the local offset is lost,
The local matrix is taken in UIElement.UpdateWorldMatrix(), where the offsets are applied, and the result is used to compute the world-space matrix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local position is stored in the RenderOffsets property of the UIElement. The world matrix is calculated in the UpdateWorldMatrix method that adds the local matrix with the render offsets and size.

So I agree that the bot comment is not valid.

}
}
Loading