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 @@ - - - - - - - - - - - - +