From c11b8ccbebff6a1e5a35251d2262feb9b196af64 Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Fri, 23 Jan 2026 18:08:51 +0100
Subject: [PATCH 1/9] feat: ImageElement can rotate the image
Add a rotation property to the ImageElement that allows to rotate the image in radians.
Introduce AngleEditor control for editing rotation properties in degrees while storing values in radians.
---
.../DefaultPropertyTemplateProviders.xaml | 14 ++
.../RotationFloatPropertyTemplateProvider.cs | 23 +++
.../Layering/ImageElementRotationTests.cs | 188 ++++++++++++++++++
.../Layering/ImageElementTests.cs | 1 +
.../Stride.UI.Tests.Windows.csproj | 1 +
.../engine/Stride.UI/Controls/ImageElement.cs | 38 +++-
.../Controls/AngleEditor.cs | 81 ++++++++
.../Themes/ThemeSelector.xaml | 26 +++
8 files changed, 371 insertions(+), 1 deletion(-)
create mode 100644 sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs
create mode 100644 sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
create mode 100644 sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
index a4f4059f60..cbdb039d97 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
+++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
@@ -1217,6 +1217,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs
new file mode 100644
index 0000000000..003168027d
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs
@@ -0,0 +1,23 @@
+// 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.Assets.Editor.View;
+using Stride.Core.Presentation.Quantum.ViewModels;
+
+namespace Stride.Core.Assets.Editor.View.TemplateProviders
+{
+ ///
+ /// Template provider that matches float properties named "Rotation" to display them in degrees using AngleEditor.
+ ///
+ public class RotationFloatPropertyTemplateProvider : NodeViewModelTemplateProvider
+ {
+ ///
+ public override string Name => "RotationFloatProperty";
+
+ ///
+ public override bool MatchNode(NodeViewModel node)
+ {
+ // Match float properties named "Rotation"
+ return node.Type == typeof(float) && node.Name == "Rotation";
+ }
+ }
+}
diff --git a/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs b/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
new file mode 100644
index 0000000000..db82120151
--- /dev/null
+++ b/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
@@ -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
+{
+ ///
+ /// Tests for the property.
+ ///
+ [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);
+ }
+
+ ///
+ /// Helper method to assert that two matrices are approximately equal within a tolerance.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/sources/engine/Stride.UI.Tests/Layering/ImageElementTests.cs b/sources/engine/Stride.UI.Tests/Layering/ImageElementTests.cs
index 39aae1d235..ccce15679f 100644
--- a/sources/engine/Stride.UI.Tests/Layering/ImageElementTests.cs
+++ b/sources/engine/Stride.UI.Tests/Layering/ImageElementTests.cs
@@ -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
}
diff --git a/sources/engine/Stride.UI.Tests/Stride.UI.Tests.Windows.csproj b/sources/engine/Stride.UI.Tests/Stride.UI.Tests.Windows.csproj
index 6f554a3615..c9a8397a79 100644
--- a/sources/engine/Stride.UI.Tests/Stride.UI.Tests.Windows.csproj
+++ b/sources/engine/Stride.UI.Tests/Stride.UI.Tests.Windows.csproj
@@ -42,6 +42,7 @@
+
diff --git a/sources/engine/Stride.UI/Controls/ImageElement.cs b/sources/engine/Stride.UI/Controls/ImageElement.cs
index d8a5270dfc..bb732f6472 100644
--- a/sources/engine/Stride.UI/Controls/ImageElement.cs
+++ b/sources/engine/Stride.UI/Controls/ImageElement.cs
@@ -32,7 +32,7 @@ public class ImageElement : UIElement
[DefaultValue(null)]
public ISpriteProvider Source
{
- get { return source;}
+ get { return source; }
set
{
if (source == value)
@@ -86,6 +86,27 @@ public StretchDirection StretchDirection
}
}
+ ///
+ /// Gets or sets the rotation angle in radians (clockwise around Z-axis).
+ ///
+ /// The rotation is applied around the center of the image. Positive values rotate clockwise.
+ /// The rotation angle in radians. Positive values rotate clockwise.
+ [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);
@@ -125,5 +146,20 @@ private void OnSpriteChanged(Sprite currentSprite)
sprite.BorderChanged += InvalidateMeasure;
}
}
+
+ ///
+ /// Updates the local transformation matrix based on the current rotation angle.
+ ///
+ private void UpdateLocalMatrix()
+ {
+ if (Math.Abs(Rotation) < float.Epsilon)
+ {
+ LocalMatrix = Matrix.Identity;
+ }
+ else
+ {
+ LocalMatrix = Matrix.RotationZ(Rotation);
+ }
+ }
}
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
new file mode 100644
index 0000000000..ba8fbdbe50
--- /dev/null
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
@@ -0,0 +1,81 @@
+// 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 System;
+using System.Windows;
+
+using Stride.Core.Mathematics;
+
+namespace Stride.Core.Presentation.Controls
+{
+ ///
+ /// Represents a control that allows to edit an angle value stored in radians but displayed in degrees.
+ ///
+ public class AngleEditor : VectorEditorBase
+ {
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty DegreesProperty = DependencyProperty.Register(
+ nameof(Degrees),
+ typeof(float),
+ typeof(AngleEditor),
+ new FrameworkPropertyMetadata(0f, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnComponentPropertyChanged, CoerceComponentValue));
+
+ ///
+ /// Gets or sets the angle in degrees.
+ ///
+ public float Degrees
+ {
+ get => (float)GetValue(DegreesProperty);
+ set => SetValue(DegreesProperty, value);
+ }
+
+ ///
+ public override void ResetValue()
+ {
+ Value = DefaultValue;
+ }
+
+ ///
+ protected override void UpdateComponentsFromValue(float value)
+ {
+ var degrees = GetDisplayValue(value);
+ SetCurrentValue(DegreesProperty, degrees);
+ }
+
+ ///
+ protected override float UpdateValueFromComponent(DependencyProperty property)
+ {
+ if (property == DegreesProperty)
+ {
+ return MathUtil.DegreesToRadians(Degrees);
+ }
+
+ throw new ArgumentException("Property unsupported by method UpdateValueFromComponent.");
+ }
+
+ ///
+ protected override float UpateValueFromFloat(float value)
+ {
+ return MathUtil.DegreesToRadians(value);
+ }
+
+ ///
+ /// Converts radians to degrees for display, handling edge cases like -0.
+ ///
+ /// The angle in radians.
+ /// The angle in degrees.
+ private static float GetDisplayValue(float angleRadians)
+ {
+ var degrees = MathUtil.RadiansToDegrees(angleRadians);
+
+ // Normalize -0 to +0 for cleaner display
+ if (degrees == 0 && float.IsNegative(degrees))
+ {
+ degrees = 0;
+ }
+
+ return degrees;
+ }
+ }
+}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml b/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
index 1d5a6b0432..e138f5bade 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
@@ -4828,4 +4828,30 @@
+
+
From f00fcba00185b689cb52c3134beec5b361fece15 Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Thu, 29 Jan 2026 16:54:45 +0100
Subject: [PATCH 2/9] Fix typo: rename UpateValueFromFloat to
UpdateValueFromFloat
Corrected a spelling error in method names across multiple files,
renaming UpateValueFromFloat to UpdateValueFromFloat. Updated all
declarations, overrides, and usages to improve code clarity and
consistency. No functional changes were made.
---
.../AssetEditors/UIEditor/Views/ThicknessEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Int2Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Int3Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Int4Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/MatrixEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/RectangleEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/RectangleFEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/RotationEditor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Vector2Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Vector3Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/Vector4Editor.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/VectorEditorBase.cs | 4 ++--
13 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Views/ThicknessEditor.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Views/ThicknessEditor.cs
index 61a0ca8bc8..3ea910770d 100644
--- a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Views/ThicknessEditor.cs
+++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Views/ThicknessEditor.cs
@@ -110,7 +110,7 @@ protected override void UpdateComponentsFromValue(Thickness? value)
}
///
- protected override Thickness? UpateValueFromFloat(float value)
+ protected override Thickness? UpdateValueFromFloat(float value)
{
return Thickness.UniformCuboid(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
index ba8fbdbe50..33a2ec0339 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
@@ -55,7 +55,7 @@ protected override float UpdateValueFromComponent(DependencyProperty property)
}
///
- protected override float UpateValueFromFloat(float value)
+ protected override float UpdateValueFromFloat(float value)
{
return MathUtil.DegreesToRadians(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int2Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int2Editor.cs
index 6b28703071..f56dbe8891 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int2Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int2Editor.cs
@@ -51,7 +51,7 @@ protected override void UpdateComponentsFromValue(Int2? value)
}
///
- protected override Int2? UpateValueFromFloat(float value)
+ protected override Int2? UpdateValueFromFloat(float value)
{
return new Int2((int)Math.Round(value, MidpointRounding.AwayFromZero));
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int3Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int3Editor.cs
index 81e876594a..4a1fad5b49 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int3Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int3Editor.cs
@@ -64,7 +64,7 @@ protected override void UpdateComponentsFromValue(Int3? value)
}
///
- protected override Int3? UpateValueFromFloat(float value)
+ protected override Int3? UpdateValueFromFloat(float value)
{
return new Int3((int)Math.Round(value, MidpointRounding.AwayFromZero));
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int4Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int4Editor.cs
index c71d56d7d7..11e1943554 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int4Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Int4Editor.cs
@@ -77,7 +77,7 @@ protected override void UpdateComponentsFromValue(Int4? value)
}
///
- protected override Int4? UpateValueFromFloat(float value)
+ protected override Int4? UpdateValueFromFloat(float value)
{
return new Int4((int)Math.Round(value, MidpointRounding.AwayFromZero));
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/MatrixEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/MatrixEditor.cs
index 2af4942043..2b77b3c893 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/MatrixEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/MatrixEditor.cs
@@ -207,7 +207,7 @@ protected override void UpdateComponentsFromValue(Matrix? value)
return new Matrix(array);
}
- protected override Matrix? UpateValueFromFloat(float value)
+ protected override Matrix? UpdateValueFromFloat(float value)
{
return new Matrix(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleEditor.cs
index 4555267aa6..c4bdce0355 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleEditor.cs
@@ -77,7 +77,7 @@ protected override void UpdateComponentsFromValue(Rectangle? value)
}
///
- protected override Rectangle? UpateValueFromFloat(float value)
+ protected override Rectangle? UpdateValueFromFloat(float value)
{
var intValue = (int)Math.Round(value, MidpointRounding.AwayFromZero);
return new Rectangle(0, 0, intValue, intValue);
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleFEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleFEditor.cs
index 4c540cc116..b81ae8e59d 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleFEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RectangleFEditor.cs
@@ -77,7 +77,7 @@ protected override void UpdateComponentsFromValue(RectangleF? value)
}
///
- protected override RectangleF? UpateValueFromFloat(float value)
+ protected override RectangleF? UpdateValueFromFloat(float value)
{
return new RectangleF(0.0f, 0.0f, value, value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RotationEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RotationEditor.cs
index 6877e5e433..6dd5bc45ec 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RotationEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/RotationEditor.cs
@@ -86,7 +86,7 @@ protected override void UpdateComponentsFromValue(Quaternion? value)
}
///
- protected override Quaternion? UpateValueFromFloat(float value)
+ protected override Quaternion? UpdateValueFromFloat(float value)
{
var radian = MathUtil.DegreesToRadians(value);
decomposedRotation = new Vector3(radian);
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector2Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector2Editor.cs
index c418f63ae7..b7ed329e1d 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector2Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector2Editor.cs
@@ -87,7 +87,7 @@ protected override void UpdateComponentsFromValue(Vector2? value)
}
///
- protected override Vector2? UpateValueFromFloat(float value)
+ protected override Vector2? UpdateValueFromFloat(float value)
{
return new Vector2(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector3Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector3Editor.cs
index 1d7089dc7a..cc1d7247ce 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector3Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector3Editor.cs
@@ -102,7 +102,7 @@ protected override void UpdateComponentsFromValue(Vector3? value)
}
///
- protected override Vector3? UpateValueFromFloat(float value)
+ protected override Vector3? UpdateValueFromFloat(float value)
{
return new Vector3(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector4Editor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector4Editor.cs
index 92ea0094ae..f4b2c88c69 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector4Editor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/Vector4Editor.cs
@@ -117,7 +117,7 @@ protected override void UpdateComponentsFromValue(Vector4? value)
}
///
- protected override Vector4? UpateValueFromFloat(float value)
+ protected override Vector4? UpdateValueFromFloat(float value)
{
return new Vector4(value);
}
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/VectorEditorBase.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/VectorEditorBase.cs
index a67290bdfc..9dd2059a77 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/VectorEditorBase.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/VectorEditorBase.cs
@@ -105,7 +105,7 @@ public override void OnApplyTemplate()
///
public override void SetVectorFromValue(float value)
{
- Value = UpateValueFromFloat(value);
+ Value = UpdateValueFromFloat(value);
}
///
@@ -130,7 +130,7 @@ public override void ResetValue()
/// Updates the property from a single float.
///
/// The value to use to generate a vector.
- protected abstract T UpateValueFromFloat(float value);
+ protected abstract T UpdateValueFromFloat(float value);
///
/// Raised when the property is modified.
From 3acfd994ad0ee9200c5db644c082ff40921181ce Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:48:57 +0100
Subject: [PATCH 3/9] refactor: Replace specialized rotation property template
provider by general template provider
Replaced the custom RotationFloatPropertyTemplateProvider with a new, reusable TypeNameMatchTemplateProvider that matches both type and property name. Updated XAML to use this provider for "Rotation" float properties, simplifying and generalizing the approach for property-specific editors. This makes it easier to add similar editors in the future without extra provider classes.
---
.../View/UIPropertyTemplates.xaml | 8 ++++++++
.../View/DefaultPropertyTemplateProviders.xaml | 14 --------------
...vider.cs => TypeNameMatchTemplateProvider.cs} | 16 +++++++++-------
3 files changed, 17 insertions(+), 21 deletions(-)
rename sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/{RotationFloatPropertyTemplateProvider.cs => TypeNameMatchTemplateProvider.cs} (53%)
diff --git a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
index 1eef046c4b..2fbe14938e 100644
--- a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
+++ b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
@@ -1,13 +1,21 @@
+
+
+
+
+
+
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
index cbdb039d97..a4f4059f60 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
+++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
@@ -1217,20 +1217,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs
similarity index 53%
rename from sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs
rename to sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs
index 003168027d..890293c61e 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/RotationFloatPropertyTemplateProvider.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs
@@ -1,23 +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.Assets.Editor.View;
using Stride.Core.Presentation.Quantum.ViewModels;
namespace Stride.Core.Assets.Editor.View.TemplateProviders
{
///
- /// Template provider that matches float properties named "Rotation" to display them in degrees using AngleEditor.
+ /// A template provider that matches nodes based on both their type and name.
///
- public class RotationFloatPropertyTemplateProvider : NodeViewModelTemplateProvider
+ public class TypeNameMatchTemplateProvider : TypeMatchTemplateProvider
{
///
- public override string Name => "RotationFloatProperty";
+ public override string Name => $"{base.Name}_{PropertyName}";
+
+ ///
+ /// Gets or sets the name of the property to match.
+ ///
+ public string PropertyName { get; set; }
- ///
public override bool MatchNode(NodeViewModel node)
{
- // Match float properties named "Rotation"
- return node.Type == typeof(float) && node.Name == "Rotation";
+ return base.MatchNode(node) && node.Name == PropertyName;
}
}
}
From 73a7656693173f6d631b477d34d3c3bde04da66c Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Fri, 6 Feb 2026 17:30:28 +0100
Subject: [PATCH 4/9] Rename template provider namename
Replaces TypeNameMatchTemplateProvider with TypeAndPropertyNameMatchTemplateProvider in both XAML and C# to clarify that matching is based on both type and property name, not just type and name. Updates all relevant usages and class definitions accordingly.
---
.../View/UIPropertyTemplates.xaml | 8 ++------
...der.cs => TypeAndPropertyNameMatchTemplateProvider.cs} | 4 ++--
2 files changed, 4 insertions(+), 8 deletions(-)
rename sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/{TypeNameMatchTemplateProvider.cs => TypeAndPropertyNameMatchTemplateProvider.cs} (88%)
diff --git a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
index 2fbe14938e..b2928eb1f7 100644
--- a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
+++ b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml
@@ -10,11 +10,11 @@
-
+
-
+
@@ -91,8 +91,4 @@
-
-
-
-
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
similarity index 88%
rename from sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs
rename to sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
index 890293c61e..d7d1489425 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeNameMatchTemplateProvider.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
@@ -5,9 +5,9 @@
namespace Stride.Core.Assets.Editor.View.TemplateProviders
{
///
- /// A template provider that matches nodes based on both their type and name.
+ /// A template provider that matches nodes based on both their type and property name.
///
- public class TypeNameMatchTemplateProvider : TypeMatchTemplateProvider
+ public class TypeAndPropertyNameMatchTemplateProvider : TypeMatchTemplateProvider
{
///
public override string Name => $"{base.Name}_{PropertyName}";
From e8341380f90055b3ed806196bffe1722040024ba Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Fri, 6 Feb 2026 17:34:31 +0100
Subject: [PATCH 5/9] Simplify floating-point comparisons in ImageElement
Refactored the Rotation property and UpdateLocalMatrix method to use direct equality checks (==) instead of Math.Abs(... - ...) < float.Epsilon. This streamlines the code but may affect floating-point precision handling.
---
sources/engine/Stride.UI/Controls/ImageElement.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/sources/engine/Stride.UI/Controls/ImageElement.cs b/sources/engine/Stride.UI/Controls/ImageElement.cs
index bb732f6472..819709bca0 100644
--- a/sources/engine/Stride.UI/Controls/ImageElement.cs
+++ b/sources/engine/Stride.UI/Controls/ImageElement.cs
@@ -99,8 +99,10 @@ public float Rotation
get { return field; }
set
{
- if (Math.Abs(field - value) < float.Epsilon)
+ if (field == value)
+ {
return;
+ }
field = value;
UpdateLocalMatrix();
@@ -152,7 +154,7 @@ private void OnSpriteChanged(Sprite currentSprite)
///
private void UpdateLocalMatrix()
{
- if (Math.Abs(Rotation) < float.Epsilon)
+ if (Rotation == 0)
{
LocalMatrix = Matrix.Identity;
}
From 28273eab9b6f5bd9daf0f352688339df77a50ab8 Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Fri, 6 Feb 2026 17:47:05 +0100
Subject: [PATCH 6/9] Simplify angle display value conversion and rounding
Refactored GetDisplayValue to remove special handling for negative zero and instead round the degree value to four decimal places for cleaner display.
---
.../Controls/AngleEditor.cs | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
index 33a2ec0339..ae5698d3be 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
@@ -61,21 +61,13 @@ protected override float UpdateValueFromFloat(float value)
}
///
- /// Converts radians to degrees for display, handling edge cases like -0.
+ /// Converts radians to degrees for display.
///
/// The angle in radians.
/// The angle in degrees.
private static float GetDisplayValue(float angleRadians)
{
- var degrees = MathUtil.RadiansToDegrees(angleRadians);
-
- // Normalize -0 to +0 for cleaner display
- if (degrees == 0 && float.IsNegative(degrees))
- {
- degrees = 0;
- }
-
- return degrees;
+ return MathF.Round(MathUtil.RadiansToDegrees(angleRadians), 4);
}
}
}
From b03f1e9ca27d4d026b8e32b360c8926b44b9460f Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Mon, 9 Feb 2026 16:01:11 +0100
Subject: [PATCH 7/9] docs(license): update copyright headers for MIT license
- Remove reference to "Silicon Studio Corp." in file headers.
- Add notice about MIT license distribution and link to LICENSE.md.
- Clarify licensing information; no functional code changes made.
---
.../TypeAndPropertyNameMatchTemplateProvider.cs | 2 +-
.../Stride.UI.Tests/Layering/ImageElementRotationTests.cs | 2 +-
.../Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
index d7d1489425..5d3334cf4e 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/View/TemplateProviders/TypeAndPropertyNameMatchTemplateProvider.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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;
diff --git a/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs b/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
index db82120151..da3ce6b215 100644
--- a/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
+++ b/sources/engine/Stride.UI.Tests/Layering/ImageElementRotationTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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;
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
index ae5698d3be..b6cde23649 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Controls/AngleEditor.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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 System;
using System.Windows;
From c0bef49578430ff8c916664166d1ee4832dfc573 Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Mon, 9 Feb 2026 16:01:30 +0100
Subject: [PATCH 8/9] fix(ui-controls): improve Rotation property precision
check
- Update Rotation setter to use floating-point tolerance comparison.
- Prevent unnecessary updates when new value is nearly equal to current.
- Address floating-point precision issues for more reliable property changes.
---
sources/engine/Stride.UI/Controls/ImageElement.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sources/engine/Stride.UI/Controls/ImageElement.cs b/sources/engine/Stride.UI/Controls/ImageElement.cs
index 819709bca0..9f840e3863 100644
--- a/sources/engine/Stride.UI/Controls/ImageElement.cs
+++ b/sources/engine/Stride.UI/Controls/ImageElement.cs
@@ -99,7 +99,7 @@ public float Rotation
get { return field; }
set
{
- if (field == value)
+ if (Math.Abs(field - value) <= MathUtil.ZeroTolerance)
{
return;
}
From a330e95e102e27fc3552f9c4da1de1e93382ca87 Mon Sep 17 00:00:00 2001
From: Peter Laske <37439758+laske185@users.noreply.github.com>
Date: Mon, 9 Feb 2026 16:18:38 +0100
Subject: [PATCH 9/9] refactor(AngleEditor): simplify control template layout
- Removed Grid, Border, DockPanel, and degree symbol from the template.
- Now uses only a NumericTextBox for a cleaner and more minimal UI.
- Adjusted NumericTextBox margin from "3,0" to "2,0" for improved spacing.
- Streamlines the AngleEditor appearance and reduces unnecessary elements.
---
.../Themes/ThemeSelector.xaml | 21 +++++--------------
1 file changed, 5 insertions(+), 16 deletions(-)
diff --git a/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml b/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
index e138f5bade..ce8c5a2658 100644
--- a/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
+++ b/sources/presentation/Stride.Core.Presentation.Wpf/Themes/ThemeSelector.xaml
@@ -4834,22 +4834,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
+