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:TypeAndPropertyNameMatchTemplateProvider x:Key="RotationTemplateProvider" Type="{x:Type s:Single}" PropertyName="Rotation" sd:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate>
<sd:AngleEditor Value="{Binding NodeValue}" />
</DataTemplate>
</tp:TypeAndPropertyNameMatchTemplateProvider>

<sd:TypeMatchTemplateProvider x:Key="StripDefinitionTemplateProvider" Type="{x:Type ui:StripDefinition}" sd:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate>
<UniformGrid Rows="1">
Expand Down Expand Up @@ -83,8 +91,4 @@
<views:ThicknessEditor Value="{Binding NodeValue}" DecimalPlaces="3" />
</DataTemplate>
</sd:TypeMatchTemplateProvider>

</ResourceDictionary>



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)
// 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 property name.
/// </summary>
public class TypeAndPropertyNameMatchTemplateProvider : 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)
// 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
40 changes: 39 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,29 @@ 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) <= MathUtil.ZeroTolerance)
{
return;
}

field = value;
UpdateLocalMatrix();
}
}

protected override Vector3 ArrangeOverride(Vector3 finalSizeWithoutMargins)
{
return ImageSizeHelper.CalculateImageSizeFromAvailable(sprite, finalSizeWithoutMargins, StretchType, StretchDirection, false);
Expand Down Expand Up @@ -125,5 +148,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 (Rotation == 0)
{
LocalMatrix = Matrix.Identity;
}
else
{
LocalMatrix = Matrix.RotationZ(Rotation);
}
}
}
}
Loading
Loading