diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
index 7d324a84af59..c6a24bab687f 100644
--- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
+++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
@@ -4852,6 +4852,14 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -8797,6 +8805,12 @@
LoadedImageSurfaceTests.xaml
+
+ PlaneProjection_Basic.xaml
+
+
+ Matrix3DProjection_Basic.xaml
+
ArcSegmentPage.xaml
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml
new file mode 100644
index 000000000000..fc937c746c41
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml.cs
new file mode 100644
index 000000000000..c9a95b9bd4b5
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/Matrix3DProjection_Basic.xaml.cs
@@ -0,0 +1,84 @@
+using System;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Media3D;
+using Uno.UI.Samples.Controls;
+
+namespace UITests.Shared.Windows_UI_Xaml_Media.Projection;
+
+[Sample("Projection", Name = "Matrix3DProjection_Basic", Description = "Matrix3DProjection with custom 4x4 transformation matrix")]
+public sealed partial class Matrix3DProjection_Basic : Page
+{
+ public Matrix3DProjection_Basic()
+ {
+ this.InitializeComponent();
+ }
+
+ private void OnApplyIdentity(object sender, RoutedEventArgs e)
+ {
+ IdentityRect.Projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = Matrix3D.Identity
+ };
+ }
+
+ private void OnApplyTranslation(object sender, RoutedEventArgs e)
+ {
+ var translateX = TranslateXSlider.Value;
+ var translateY = TranslateYSlider.Value;
+ var translateZ = TranslateZSlider.Value;
+
+ // Create a translation matrix
+ var matrix = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ translateX, translateY, translateZ, 1);
+
+ TranslationRect.Projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = matrix
+ };
+ }
+
+ private void OnApplyScale(object sender, RoutedEventArgs e)
+ {
+ var scaleX = ScaleXSlider.Value;
+ var scaleY = ScaleYSlider.Value;
+
+ // Create a scale matrix
+ var matrix = new Matrix3D(
+ scaleX, 0, 0, 0,
+ 0, scaleY, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1);
+
+ ScaleRect.Projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = matrix
+ };
+ }
+
+ private void OnApplyPerspective(object sender, RoutedEventArgs e)
+ {
+ var perspective = PerspectiveSlider.Value;
+ var rotationY = PerspectiveRotationY.Value * Math.PI / 180.0;
+
+ // Create rotation Y matrix
+ var cosY = Math.Cos(rotationY);
+ var sinY = Math.Sin(rotationY);
+
+ // Combine rotation with perspective
+ var matrix = new Matrix3D(
+ cosY, 0, sinY, 0,
+ 0, 1, 0, 0,
+ -sinY, 0, cosY, perspective,
+ 0, 0, 0, 1);
+
+ PerspectiveRect.Projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = matrix
+ };
+ }
+}
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml
new file mode 100644
index 000000000000..cfd3a2454d89
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml
@@ -0,0 +1,430 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml.cs
new file mode 100644
index 000000000000..eb20f0716db8
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/Projection/PlaneProjection_Basic.xaml.cs
@@ -0,0 +1,13 @@
+using Microsoft.UI.Xaml.Controls;
+using Uno.UI.Samples.Controls;
+
+namespace UITests.Shared.Windows_UI_Xaml_Media.Projection;
+
+[Sample("Projection", Name = "PlaneProjection_Basic", Description = "Basic PlaneProjection 3D transforms with interactive controls")]
+public sealed partial class PlaneProjection_Basic : Page
+{
+ public PlaneProjection_Basic()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3D.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3D.cs
new file mode 100644
index 000000000000..159521d4311c
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3D.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Numerics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.UI.Xaml.Media.Media3D;
+
+namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media;
+
+[TestClass]
+public class Given_Matrix3D
+{
+ [TestMethod]
+ public void When_Default_Constructor()
+ {
+ var matrix = new Matrix3D();
+
+ Assert.AreEqual(0, matrix.M11);
+ Assert.AreEqual(0, matrix.M12);
+ Assert.AreEqual(0, matrix.M13);
+ Assert.AreEqual(0, matrix.M14);
+ Assert.AreEqual(0, matrix.M21);
+ Assert.AreEqual(0, matrix.M22);
+ Assert.AreEqual(0, matrix.M23);
+ Assert.AreEqual(0, matrix.M24);
+ Assert.AreEqual(0, matrix.M31);
+ Assert.AreEqual(0, matrix.M32);
+ Assert.AreEqual(0, matrix.M33);
+ Assert.AreEqual(0, matrix.M34);
+ Assert.AreEqual(0, matrix.OffsetX);
+ Assert.AreEqual(0, matrix.OffsetY);
+ Assert.AreEqual(0, matrix.OffsetZ);
+ Assert.AreEqual(0, matrix.M44);
+ }
+
+ [TestMethod]
+ public void When_Full_Constructor()
+ {
+ var matrix = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ Assert.AreEqual(1, matrix.M11);
+ Assert.AreEqual(2, matrix.M12);
+ Assert.AreEqual(3, matrix.M13);
+ Assert.AreEqual(4, matrix.M14);
+ Assert.AreEqual(5, matrix.M21);
+ Assert.AreEqual(6, matrix.M22);
+ Assert.AreEqual(7, matrix.M23);
+ Assert.AreEqual(8, matrix.M24);
+ Assert.AreEqual(9, matrix.M31);
+ Assert.AreEqual(10, matrix.M32);
+ Assert.AreEqual(11, matrix.M33);
+ Assert.AreEqual(12, matrix.M34);
+ Assert.AreEqual(13, matrix.OffsetX);
+ Assert.AreEqual(14, matrix.OffsetY);
+ Assert.AreEqual(15, matrix.OffsetZ);
+ Assert.AreEqual(16, matrix.M44);
+ }
+
+ [TestMethod]
+ public void When_Identity()
+ {
+ var identity = Matrix3D.Identity;
+
+ Assert.AreEqual(1, identity.M11);
+ Assert.AreEqual(0, identity.M12);
+ Assert.AreEqual(0, identity.M13);
+ Assert.AreEqual(0, identity.M14);
+ Assert.AreEqual(0, identity.M21);
+ Assert.AreEqual(1, identity.M22);
+ Assert.AreEqual(0, identity.M23);
+ Assert.AreEqual(0, identity.M24);
+ Assert.AreEqual(0, identity.M31);
+ Assert.AreEqual(0, identity.M32);
+ Assert.AreEqual(1, identity.M33);
+ Assert.AreEqual(0, identity.M34);
+ Assert.AreEqual(0, identity.OffsetX);
+ Assert.AreEqual(0, identity.OffsetY);
+ Assert.AreEqual(0, identity.OffsetZ);
+ Assert.AreEqual(1, identity.M44);
+ }
+
+ [TestMethod]
+ public void When_IsIdentity_True()
+ {
+ var identity = Matrix3D.Identity;
+ Assert.IsTrue(identity.IsIdentity);
+ }
+
+ [TestMethod]
+ public void When_IsIdentity_False()
+ {
+ var matrix = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 10, 0, 0, 1);
+
+ Assert.IsFalse(matrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public void When_Equality_True()
+ {
+ var matrix1 = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ var matrix2 = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ Assert.IsTrue(matrix1 == matrix2);
+ Assert.IsTrue(matrix1.Equals(matrix2));
+ Assert.AreEqual(matrix1, matrix2);
+ }
+
+ [TestMethod]
+ public void When_Equality_False()
+ {
+ var matrix1 = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ var matrix2 = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 17); // Different M44
+
+ Assert.IsTrue(matrix1 != matrix2);
+ Assert.IsFalse(matrix1.Equals(matrix2));
+ Assert.AreNotEqual(matrix1, matrix2);
+ }
+
+ [TestMethod]
+ public void When_Multiply_Identity()
+ {
+ var matrix = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ var result = matrix * Matrix3D.Identity;
+
+ Assert.AreEqual(matrix, result);
+ }
+
+ [TestMethod]
+ public void When_Multiply_Translation()
+ {
+ var identity = Matrix3D.Identity;
+ var translation = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 10, 20, 30, 1);
+
+ var result = identity * translation;
+
+ Assert.AreEqual(10, result.OffsetX);
+ Assert.AreEqual(20, result.OffsetY);
+ Assert.AreEqual(30, result.OffsetZ);
+ }
+
+ [TestMethod]
+ public void When_HasInverse_True()
+ {
+ var identity = Matrix3D.Identity;
+ Assert.IsTrue(identity.HasInverse);
+ }
+
+ [TestMethod]
+ public void When_HasInverse_False()
+ {
+ // Singular matrix (all zeros)
+ var singular = new Matrix3D();
+ Assert.IsFalse(singular.HasInverse);
+ }
+
+ [TestMethod]
+ public void When_Invert_Identity()
+ {
+ var matrix = Matrix3D.Identity;
+ matrix.Invert();
+
+ Assert.IsTrue(matrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public void When_Invert_Translation()
+ {
+ var matrix = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 10, 20, 30, 1);
+
+ matrix.Invert();
+
+ Assert.AreEqual(-10, matrix.OffsetX, 1e-10);
+ Assert.AreEqual(-20, matrix.OffsetY, 1e-10);
+ Assert.AreEqual(-30, matrix.OffsetZ, 1e-10);
+ }
+
+ [TestMethod]
+ public void When_Invert_Singular_Throws()
+ {
+ var singular = new Matrix3D();
+ Assert.ThrowsExactly(() => singular.Invert());
+ }
+
+ [TestMethod]
+ public void When_ToString()
+ {
+ var identity = Matrix3D.Identity;
+ var str = identity.ToString();
+
+ Assert.IsNotNull(str);
+ Assert.IsTrue(str.Contains("1"));
+ }
+
+#if HAS_UNO
+ [TestMethod]
+ public void When_ToMatrix4x4_And_Back()
+ {
+ var original = new Matrix3D(
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+
+ var matrix4x4 = original.ToMatrix4x4();
+ var roundTripped = Matrix3D.FromMatrix4x4(matrix4x4);
+
+ // Note: Some precision loss due to float conversion
+ Assert.AreEqual(original.M11, roundTripped.M11, 1e-5);
+ Assert.AreEqual(original.M22, roundTripped.M22, 1e-5);
+ Assert.AreEqual(original.M33, roundTripped.M33, 1e-5);
+ Assert.AreEqual(original.M44, roundTripped.M44, 1e-5);
+ }
+#endif
+}
diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3DProjection.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3DProjection.cs
new file mode 100644
index 000000000000..b7f58e4f616d
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Matrix3DProjection.cs
@@ -0,0 +1,173 @@
+#if __SKIA__
+using System;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Media3D;
+using Private.Infrastructure;
+using Uno.UI.RuntimeTests.Helpers;
+
+namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media;
+
+[TestClass]
+[RunsOnUIThread]
+public class Given_Matrix3DProjection
+{
+ [TestMethod]
+ public void When_Default_Values()
+ {
+ var projection = new Matrix3DProjection();
+
+ // Default ProjectionMatrix should be default Matrix3D (all zeros)
+ var matrix = projection.ProjectionMatrix;
+ Assert.AreEqual(0, matrix.M11);
+ Assert.AreEqual(0, matrix.M22);
+ Assert.AreEqual(0, matrix.M33);
+ Assert.AreEqual(0, matrix.M44);
+ }
+
+ [TestMethod]
+ public void When_Set_ProjectionMatrix()
+ {
+ var projection = new Matrix3DProjection();
+ var matrix = Matrix3D.Identity;
+
+ projection.ProjectionMatrix = matrix;
+
+ Assert.IsTrue(projection.ProjectionMatrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public void When_Set_Custom_Matrix()
+ {
+ var projection = new Matrix3DProjection();
+ var customMatrix = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 10, 20, 30, 1);
+
+ projection.ProjectionMatrix = customMatrix;
+
+ Assert.AreEqual(10, projection.ProjectionMatrix.OffsetX);
+ Assert.AreEqual(20, projection.ProjectionMatrix.OffsetY);
+ Assert.AreEqual(30, projection.ProjectionMatrix.OffsetZ);
+ }
+
+ [TestMethod]
+ public async Task When_Applied_To_Element()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Red)
+ };
+
+ var projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = Matrix3D.Identity
+ };
+
+ border.Projection = projection;
+
+ await UITestHelper.Load(border);
+
+ Assert.AreEqual(projection, border.Projection);
+ Assert.IsTrue(((Matrix3DProjection)border.Projection).ProjectionMatrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public async Task When_Matrix_Changed_Dynamically()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Blue)
+ };
+
+ var projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = Matrix3D.Identity
+ };
+ border.Projection = projection;
+
+ await UITestHelper.Load(border);
+
+ // Change matrix after element is loaded
+ projection.ProjectionMatrix = new Matrix3D(
+ 2, 0, 0, 0,
+ 0, 2, 0, 0,
+ 0, 0, 2, 0,
+ 0, 0, 0, 1);
+
+ Assert.AreEqual(2, projection.ProjectionMatrix.M11);
+ Assert.AreEqual(2, projection.ProjectionMatrix.M22);
+ Assert.AreEqual(2, projection.ProjectionMatrix.M33);
+ }
+
+ [TestMethod]
+ public async Task When_Combined_With_RenderTransform()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Green),
+ RenderTransform = new RotateTransform { Angle = 45 },
+ Projection = new Matrix3DProjection { ProjectionMatrix = Matrix3D.Identity }
+ };
+
+ await UITestHelper.Load(border);
+
+ Assert.IsNotNull(border.RenderTransform);
+ Assert.IsNotNull(border.Projection);
+ }
+
+ [TestMethod]
+ public void When_GetProjectionMatrix()
+ {
+ var customMatrix = new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 50, 60, 70, 1);
+
+ var projection = new Matrix3DProjection
+ {
+ ProjectionMatrix = customMatrix
+ };
+
+ // GetProjectionMatrix should return the matrix converted to Matrix4x4
+ var matrix4x4 = projection.GetProjectionMatrix(new Windows.Foundation.Size(100, 100));
+
+ Assert.AreEqual(50, matrix4x4.M41, 1e-5);
+ Assert.AreEqual(60, matrix4x4.M42, 1e-5);
+ Assert.AreEqual(70, matrix4x4.M43, 1e-5);
+ }
+
+ [TestMethod]
+ public async Task When_Projection_Set_To_Null()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Yellow),
+ Projection = new Matrix3DProjection { ProjectionMatrix = Matrix3D.Identity }
+ };
+
+ await UITestHelper.Load(border);
+
+ Assert.IsNotNull(border.Projection);
+
+ // Remove projection
+ border.Projection = null;
+
+ Assert.IsNull(border.Projection);
+ }
+}
+#endif
diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_PlaneProjection.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_PlaneProjection.cs
new file mode 100644
index 000000000000..79c08d31e3b9
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_PlaneProjection.cs
@@ -0,0 +1,220 @@
+#if __SKIA__
+using System;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Private.Infrastructure;
+using Uno.UI.RuntimeTests.Helpers;
+
+namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media;
+
+[TestClass]
+[RunsOnUIThread]
+public class Given_PlaneProjection
+{
+ [TestMethod]
+ public void When_Default_Values()
+ {
+ var projection = new PlaneProjection();
+
+ // CenterOfRotationX and CenterOfRotationY default to 0.5 (center of element)
+ Assert.AreEqual(0.5, projection.CenterOfRotationX);
+ Assert.AreEqual(0.5, projection.CenterOfRotationY);
+ Assert.AreEqual(0.0, projection.CenterOfRotationZ);
+
+ // All rotations default to 0
+ Assert.AreEqual(0.0, projection.RotationX);
+ Assert.AreEqual(0.0, projection.RotationY);
+ Assert.AreEqual(0.0, projection.RotationZ);
+
+ // All offsets default to 0
+ Assert.AreEqual(0.0, projection.LocalOffsetX);
+ Assert.AreEqual(0.0, projection.LocalOffsetY);
+ Assert.AreEqual(0.0, projection.LocalOffsetZ);
+ Assert.AreEqual(0.0, projection.GlobalOffsetX);
+ Assert.AreEqual(0.0, projection.GlobalOffsetY);
+ Assert.AreEqual(0.0, projection.GlobalOffsetZ);
+ }
+
+ [TestMethod]
+ public void When_Set_RotationX()
+ {
+ var projection = new PlaneProjection();
+ projection.RotationX = 45;
+
+ Assert.AreEqual(45, projection.RotationX);
+ }
+
+ [TestMethod]
+ public void When_Set_RotationY()
+ {
+ var projection = new PlaneProjection();
+ projection.RotationY = 90;
+
+ Assert.AreEqual(90, projection.RotationY);
+ }
+
+ [TestMethod]
+ public void When_Set_RotationZ()
+ {
+ var projection = new PlaneProjection();
+ projection.RotationZ = 180;
+
+ Assert.AreEqual(180, projection.RotationZ);
+ }
+
+ [TestMethod]
+ public void When_Set_LocalOffset()
+ {
+ var projection = new PlaneProjection();
+ projection.LocalOffsetX = 10;
+ projection.LocalOffsetY = 20;
+ projection.LocalOffsetZ = 30;
+
+ Assert.AreEqual(10, projection.LocalOffsetX);
+ Assert.AreEqual(20, projection.LocalOffsetY);
+ Assert.AreEqual(30, projection.LocalOffsetZ);
+ }
+
+ [TestMethod]
+ public void When_Set_GlobalOffset()
+ {
+ var projection = new PlaneProjection();
+ projection.GlobalOffsetX = 100;
+ projection.GlobalOffsetY = 200;
+ projection.GlobalOffsetZ = 300;
+
+ Assert.AreEqual(100, projection.GlobalOffsetX);
+ Assert.AreEqual(200, projection.GlobalOffsetY);
+ Assert.AreEqual(300, projection.GlobalOffsetZ);
+ }
+
+ [TestMethod]
+ public void When_Set_CenterOfRotation()
+ {
+ var projection = new PlaneProjection();
+ projection.CenterOfRotationX = 0.0; // Left edge
+ projection.CenterOfRotationY = 1.0; // Bottom edge
+ projection.CenterOfRotationZ = 50;
+
+ Assert.AreEqual(0.0, projection.CenterOfRotationX);
+ Assert.AreEqual(1.0, projection.CenterOfRotationY);
+ Assert.AreEqual(50, projection.CenterOfRotationZ);
+ }
+
+ [TestMethod]
+ public async Task When_Applied_To_Element()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Red)
+ };
+
+ var projection = new PlaneProjection
+ {
+ RotationY = 45
+ };
+
+ border.Projection = projection;
+
+ await UITestHelper.Load(border);
+
+ Assert.AreEqual(projection, border.Projection);
+ Assert.AreEqual(45, ((PlaneProjection)border.Projection).RotationY);
+ }
+
+ [TestMethod]
+ public async Task When_Projection_Changed_Dynamically()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Blue)
+ };
+
+ var projection = new PlaneProjection();
+ border.Projection = projection;
+
+ await UITestHelper.Load(border);
+
+ // Change rotation after element is loaded
+ projection.RotationX = 30;
+ projection.RotationY = 60;
+
+ Assert.AreEqual(30, projection.RotationX);
+ Assert.AreEqual(60, projection.RotationY);
+ }
+
+ [TestMethod]
+ public async Task When_Combined_With_RenderTransform()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Green),
+ RenderTransform = new ScaleTransform { ScaleX = 1.5, ScaleY = 1.5 },
+ Projection = new PlaneProjection { RotationY = 45 }
+ };
+
+ await UITestHelper.Load(border);
+
+ Assert.IsNotNull(border.RenderTransform);
+ Assert.IsNotNull(border.Projection);
+ Assert.AreEqual(45, ((PlaneProjection)border.Projection).RotationY);
+ }
+
+ [TestMethod]
+ public void When_ProjectionMatrix_Is_Computed()
+ {
+ var projection = new PlaneProjection
+ {
+ RotationY = 45
+ };
+
+ // Trigger matrix computation by getting the projection matrix
+ var matrix = projection.GetProjectionMatrix(new Windows.Foundation.Size(100, 100));
+
+ // The matrix should not be identity when rotation is applied
+ Assert.IsFalse(matrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public void When_No_Rotation_Matrix_Is_Identity()
+ {
+ var projection = new PlaneProjection();
+
+ // With all defaults (no rotation, no offsets), Is2DAligned() returns true
+ // and the projection matrix is identity (no 3D effect applied).
+ var matrix = projection.GetProjectionMatrix(new Windows.Foundation.Size(100, 100));
+
+ Assert.IsTrue(matrix.IsIdentity);
+ }
+
+ [TestMethod]
+ public async Task When_Projection_Set_To_Null()
+ {
+ var border = new Border
+ {
+ Width = 100,
+ Height = 100,
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Yellow),
+ Projection = new PlaneProjection { RotationZ = 45 }
+ };
+
+ await UITestHelper.Load(border);
+
+ Assert.IsNotNull(border.Projection);
+
+ // Remove projection
+ border.Projection = null;
+
+ Assert.IsNull(border.Projection);
+ }
+}
+#endif
diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Matrix3DProjection.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Matrix3DProjection.cs
index aa1d127d0d47..2196e289a547 100644
--- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Matrix3DProjection.cs
+++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Matrix3DProjection.cs
@@ -3,14 +3,16 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Media
{
+#if !__SKIA__
[global::Microsoft.UI.Xaml.Markup.ContentPropertyAttribute(Name = "ProjectionMatrix")]
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
+#endif
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented]
#endif
public partial class Matrix3DProjection : global::Microsoft.UI.Xaml.Media.Projection
{
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D ProjectionMatrix
{
get
@@ -23,16 +25,16 @@ public partial class Matrix3DProjection : global::Microsoft.UI.Xaml.Media.Projec
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty ProjectionMatrixProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(ProjectionMatrix), typeof(global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D),
typeof(global::Microsoft.UI.Xaml.Media.Matrix3DProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public Matrix3DProjection() : base()
{
global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Microsoft.UI.Xaml.Media.Matrix3DProjection", "Matrix3DProjection.Matrix3DProjection()");
diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/PlaneProjection.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/PlaneProjection.cs
index 5aef17852423..ebf57d463af2 100644
--- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/PlaneProjection.cs
+++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/PlaneProjection.cs
@@ -3,13 +3,13 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Media
{
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented]
#endif
public partial class PlaneProjection : global::Microsoft.UI.Xaml.Media.Projection
{
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double RotationZ
{
get
@@ -22,8 +22,8 @@ public double RotationZ
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double RotationY
{
get
@@ -36,8 +36,8 @@ public double RotationY
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double RotationX
{
get
@@ -50,8 +50,8 @@ public double RotationX
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double LocalOffsetZ
{
get
@@ -64,8 +64,8 @@ public double LocalOffsetZ
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double LocalOffsetY
{
get
@@ -78,8 +78,8 @@ public double LocalOffsetY
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double LocalOffsetX
{
get
@@ -92,8 +92,8 @@ public double LocalOffsetX
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double GlobalOffsetZ
{
get
@@ -106,8 +106,8 @@ public double GlobalOffsetZ
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double GlobalOffsetY
{
get
@@ -120,8 +120,8 @@ public double GlobalOffsetY
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double GlobalOffsetX
{
get
@@ -134,8 +134,8 @@ public double GlobalOffsetX
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double CenterOfRotationZ
{
get
@@ -148,8 +148,8 @@ public double CenterOfRotationZ
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double CenterOfRotationY
{
get
@@ -162,8 +162,8 @@ public double CenterOfRotationY
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public double CenterOfRotationX
{
get
@@ -176,8 +176,8 @@ public double CenterOfRotationX
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D ProjectionMatrix
{
get
@@ -186,112 +186,112 @@ public double CenterOfRotationX
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty CenterOfRotationXProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(CenterOfRotationX), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty CenterOfRotationYProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(CenterOfRotationY), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty CenterOfRotationZProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(CenterOfRotationZ), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty GlobalOffsetXProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(GlobalOffsetX), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty GlobalOffsetYProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(GlobalOffsetY), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty GlobalOffsetZProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(GlobalOffsetZ), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty LocalOffsetXProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(LocalOffsetX), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty LocalOffsetYProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(LocalOffsetY), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty LocalOffsetZProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(LocalOffsetZ), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty ProjectionMatrixProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(ProjectionMatrix), typeof(global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Microsoft.UI.Xaml.Media.Media3D.Matrix3D)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty RotationXProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(RotationX), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty RotationYProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(RotationY), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty RotationZProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(RotationZ), typeof(double),
typeof(global::Microsoft.UI.Xaml.Media.PlaneProjection),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(double)));
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public PlaneProjection() : base()
{
global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Microsoft.UI.Xaml.Media.PlaneProjection", "PlaneProjection.PlaneProjection()");
diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Projection.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Projection.cs
index 932e8d7518b5..4005f7feb9f5 100644
--- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Projection.cs
+++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Media/Projection.cs
@@ -3,13 +3,13 @@
#pragma warning disable 114 // new keyword hiding
namespace Microsoft.UI.Xaml.Media
{
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented]
#endif
public partial class Projection : global::Microsoft.UI.Xaml.DependencyObject
{
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
protected Projection() : base()
{
global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Microsoft.UI.Xaml.Media.Projection", "Projection.Projection()");
diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml/UIElement.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml/UIElement.cs
index e53650934b24..1ee20528d667 100644
--- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml/UIElement.cs
+++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml/UIElement.cs
@@ -365,8 +365,8 @@ public double RasterizationScale
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public global::Microsoft.UI.Xaml.Media.Projection Projection
{
get
@@ -657,8 +657,8 @@ public double RasterizationScale
}
}
#endif
-#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
- [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
+#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __NETSTD_REFERENCE__
+ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty ProjectionProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(Projection), typeof(global::Microsoft.UI.Xaml.Media.Projection),
diff --git a/src/Uno.UI/Media/NativeRenderTransformAdapter.skia.cs b/src/Uno.UI/Media/NativeRenderTransformAdapter.skia.cs
index aed8a5d4323f..83a3529829d2 100644
--- a/src/Uno.UI/Media/NativeRenderTransformAdapter.skia.cs
+++ b/src/Uno.UI/Media/NativeRenderTransformAdapter.skia.cs
@@ -1,9 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using Uno.Extensions;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Windows.Foundation;
namespace Uno.UI.Media
{
@@ -19,14 +21,29 @@ partial void Apply(bool isSizeChanged, bool isOriginChanged)
{
FlowDirectionTransform = Owner.GetFlowDirectionTransform();
+ // Get base 2D transform (RenderTransform + FlowDirection)
+ Matrix3x2 transform2D;
if (Transform is null)
{
- Owner.Visual.TransformMatrix = new Matrix4x4(FlowDirectionTransform);
+ transform2D = FlowDirectionTransform;
}
else
{
- Owner.Visual.TransformMatrix = new Matrix4x4(Transform.ToMatrix(CurrentOrigin, CurrentSize) * FlowDirectionTransform);
+ transform2D = Transform.ToMatrix(CurrentOrigin, CurrentSize) * FlowDirectionTransform;
}
+
+ // Convert to 4x4 matrix
+ var finalMatrix = new Matrix4x4(transform2D);
+
+ // Apply projection if set
+ if (Owner is UIElement element && element.GetProjection() is Projection projection)
+ {
+ var projectionMatrix = projection.GetProjectionMatrix(CurrentSize);
+ // Projection is applied after RenderTransform
+ finalMatrix = finalMatrix * projectionMatrix;
+ }
+
+ Owner.Visual.TransformMatrix = finalMatrix;
}
partial void Cleanup()
diff --git a/src/Uno.UI/UI/Xaml/Media/Matrix3DProjection.cs b/src/Uno.UI/UI/Xaml/Media/Matrix3DProjection.cs
new file mode 100644
index 000000000000..ed62b9f52945
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Matrix3DProjection.cs
@@ -0,0 +1,57 @@
+#if __SKIA__
+using System;
+using System.Numerics;
+using Windows.Foundation;
+using Microsoft.UI.Xaml.Media.Media3D;
+
+namespace Microsoft.UI.Xaml.Media;
+
+///
+/// Applies a Matrix3D projection to an object.
+///
+[Microsoft.UI.Xaml.Markup.ContentProperty(Name = nameof(ProjectionMatrix))]
+public partial class Matrix3DProjection : Projection
+{
+ ///
+ /// Initializes a new instance of the Matrix3DProjection class.
+ ///
+ public Matrix3DProjection()
+ {
+ }
+
+ ///
+ /// Gets or sets the Matrix3D that is used for the projection that is applied to the object.
+ ///
+ public Matrix3D ProjectionMatrix
+ {
+ get => (Matrix3D)GetValue(ProjectionMatrixProperty);
+ set => SetValue(ProjectionMatrixProperty, value);
+ }
+
+ ///
+ /// Identifies the ProjectionMatrix dependency property.
+ ///
+ public static DependencyProperty ProjectionMatrixProperty { get; } =
+ DependencyProperty.Register(
+ nameof(ProjectionMatrix),
+ typeof(Matrix3D),
+ typeof(Matrix3DProjection),
+ new FrameworkPropertyMetadata(Matrix3D.Identity, OnProjectionMatrixChanged));
+
+ private static void OnProjectionMatrixChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is Matrix3DProjection projection)
+ {
+ projection.OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Returns the projection matrix directly from the ProjectionMatrix property.
+ ///
+ internal override Matrix4x4 GetProjectionMatrix(Size elementSize)
+ {
+ return ProjectionMatrix.ToMatrix4x4();
+ }
+}
+#endif
diff --git a/src/Uno.UI/UI/Xaml/Media/Media3D/Matrix3D.cs b/src/Uno.UI/UI/Xaml/Media/Media3D/Matrix3D.cs
index 473a4cfd770c..580498eb7837 100644
--- a/src/Uno.UI/UI/Xaml/Media/Media3D/Matrix3D.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Media3D/Matrix3D.cs
@@ -1,63 +1,225 @@
-#region Assembly System.Runtime.WindowsRuntime.UI.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-// C:\Users\jerome.laban\.nuget\packages\System.Runtime.WindowsRuntime.UI.Xaml\4.0.0\ref\netcore50\System.Runtime.WindowsRuntime.UI.Xaml.dll
-#endregion
-
using System;
-using System.Security;
+using System.Numerics;
+
+namespace Microsoft.UI.Xaml.Media.Media3D;
-namespace Microsoft.UI.Xaml.Media.Media3D
+///
+/// Represents a 4 × 4 matrix that is used for transformations in a three-dimensional (3-D) space.
+///
+public partial struct Matrix3D : IFormattable
{
- //
- // Summary:
- // Represents a 4 × 4 matrix that is used for transformations in a three-dimensional
- // (3-D) space.
- [SecurityCritical]
- public partial struct Matrix3D : IFormattable
+ public double M11;
+ public double M12;
+ public double M13;
+ public double M14;
+ public double M21;
+ public double M22;
+ public double M23;
+ public double M24;
+ public double M31;
+ public double M32;
+ public double M33;
+ public double M34;
+ public double OffsetX;
+ public double OffsetY;
+ public double OffsetZ;
+ public double M44;
+
+ ///
+ /// Initializes a new instance of the Matrix3D structure.
+ ///
+ public Matrix3D(
+ double m11, double m12, double m13, double m14,
+ double m21, double m22, double m23, double m24,
+ double m31, double m32, double m33, double m34,
+ double offsetX, double offsetY, double offsetZ, double m44)
{
- public Matrix3D(double m11, double m12, double m13, double m14, double m21, double m22, double m23, double m24, double m31, double m32, double m33, double m34, double offsetX, double offsetY, double offsetZ, double m44) { throw new NotImplementedException(); }
-
- public static Matrix3D Identity { get; }
-
- public bool HasInverse { get; }
-
- public bool IsIdentity { get; }
-
- public double M11;
- public double M12;
- public double M13;
- public double M14;
- public double M21;
- public double M22;
- public double M23;
- public double M24;
- public double M31;
- public double M32;
- public double M33;
- public double M34;
- public double M44;
- public double OffsetX;
- public double OffsetY;
- public double OffsetZ;
-
- public override bool Equals(object o) { throw new NotImplementedException(); }
-
- public bool Equals(Matrix3D value) { throw new NotImplementedException(); }
+ M11 = m11;
+ M12 = m12;
+ M13 = m13;
+ M14 = m14;
+ M21 = m21;
+ M22 = m22;
+ M23 = m23;
+ M24 = m24;
+ M31 = m31;
+ M32 = m32;
+ M33 = m33;
+ M34 = m34;
+ OffsetX = offsetX;
+ OffsetY = offsetY;
+ OffsetZ = offsetZ;
+ M44 = m44;
+ }
- [SecuritySafeCritical]
- public override int GetHashCode() { throw new NotImplementedException(); }
+ ///
+ /// Gets an identity Matrix3D.
+ ///
+ public static Matrix3D Identity => new Matrix3D(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1);
+
+ ///
+ /// Gets a value that indicates whether this Matrix3D is an identity matrix.
+ ///
+ public bool IsIdentity =>
+ M11 == 1 && M12 == 0 && M13 == 0 && M14 == 0 &&
+ M21 == 0 && M22 == 1 && M23 == 0 && M24 == 0 &&
+ M31 == 0 && M32 == 0 && M33 == 1 && M34 == 0 &&
+ OffsetX == 0 && OffsetY == 0 && OffsetZ == 0 && M44 == 1;
+
+ ///
+ /// Gets a value that indicates whether this Matrix3D is invertible.
+ ///
+ public bool HasInverse => GetDeterminant() != 0;
+
+ ///
+ /// Inverts this Matrix3D structure.
+ ///
+ /// The matrix is not invertible.
+ public void Invert()
+ {
+ var matrix4x4 = ToMatrix4x4();
+ if (!Matrix4x4.Invert(matrix4x4, out var inverted))
+ {
+ throw new InvalidOperationException("Matrix is not invertible.");
+ }
+ this = FromMatrix4x4(inverted);
+ }
- public void Invert() { throw new NotImplementedException(); }
+ ///
+ /// Multiplies two Matrix3D structures.
+ ///
+ public static Matrix3D operator *(Matrix3D matrix1, Matrix3D matrix2)
+ {
+ return new Matrix3D(
+ matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21 + matrix1.M13 * matrix2.M31 + matrix1.M14 * matrix2.OffsetX,
+ matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22 + matrix1.M13 * matrix2.M32 + matrix1.M14 * matrix2.OffsetY,
+ matrix1.M11 * matrix2.M13 + matrix1.M12 * matrix2.M23 + matrix1.M13 * matrix2.M33 + matrix1.M14 * matrix2.OffsetZ,
+ matrix1.M11 * matrix2.M14 + matrix1.M12 * matrix2.M24 + matrix1.M13 * matrix2.M34 + matrix1.M14 * matrix2.M44,
+
+ matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21 + matrix1.M23 * matrix2.M31 + matrix1.M24 * matrix2.OffsetX,
+ matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22 + matrix1.M23 * matrix2.M32 + matrix1.M24 * matrix2.OffsetY,
+ matrix1.M21 * matrix2.M13 + matrix1.M22 * matrix2.M23 + matrix1.M23 * matrix2.M33 + matrix1.M24 * matrix2.OffsetZ,
+ matrix1.M21 * matrix2.M14 + matrix1.M22 * matrix2.M24 + matrix1.M23 * matrix2.M34 + matrix1.M24 * matrix2.M44,
+
+ matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix1.M33 * matrix2.M31 + matrix1.M34 * matrix2.OffsetX,
+ matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix1.M33 * matrix2.M32 + matrix1.M34 * matrix2.OffsetY,
+ matrix1.M31 * matrix2.M13 + matrix1.M32 * matrix2.M23 + matrix1.M33 * matrix2.M33 + matrix1.M34 * matrix2.OffsetZ,
+ matrix1.M31 * matrix2.M14 + matrix1.M32 * matrix2.M24 + matrix1.M33 * matrix2.M34 + matrix1.M34 * matrix2.M44,
+
+ matrix1.OffsetX * matrix2.M11 + matrix1.OffsetY * matrix2.M21 + matrix1.OffsetZ * matrix2.M31 + matrix1.M44 * matrix2.OffsetX,
+ matrix1.OffsetX * matrix2.M12 + matrix1.OffsetY * matrix2.M22 + matrix1.OffsetZ * matrix2.M32 + matrix1.M44 * matrix2.OffsetY,
+ matrix1.OffsetX * matrix2.M13 + matrix1.OffsetY * matrix2.M23 + matrix1.OffsetZ * matrix2.M33 + matrix1.M44 * matrix2.OffsetZ,
+ matrix1.OffsetX * matrix2.M14 + matrix1.OffsetY * matrix2.M24 + matrix1.OffsetZ * matrix2.M34 + matrix1.M44 * matrix2.M44
+ );
+ }
- public override string ToString() { throw new NotImplementedException(); }
+ ///
+ /// Compares two Matrix3D instances for equality.
+ ///
+ public static bool operator ==(Matrix3D matrix1, Matrix3D matrix2) => matrix1.Equals(matrix2);
+
+ ///
+ /// Compares two Matrix3D instances for inequality.
+ ///
+ public static bool operator !=(Matrix3D matrix1, Matrix3D matrix2) => !matrix1.Equals(matrix2);
+
+ ///
+ public override bool Equals(object o) => o is Matrix3D matrix && Equals(matrix);
+
+ ///
+ /// Compares two Matrix3D instances for equality.
+ ///
+ public bool Equals(Matrix3D value) =>
+ M11 == value.M11 && M12 == value.M12 && M13 == value.M13 && M14 == value.M14 &&
+ M21 == value.M21 && M22 == value.M22 && M23 == value.M23 && M24 == value.M24 &&
+ M31 == value.M31 && M32 == value.M32 && M33 == value.M33 && M34 == value.M34 &&
+ OffsetX == value.OffsetX && OffsetY == value.OffsetY && OffsetZ == value.OffsetZ && M44 == value.M44;
+
+ ///
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(M11);
+ hash.Add(M12);
+ hash.Add(M13);
+ hash.Add(M14);
+ hash.Add(M21);
+ hash.Add(M22);
+ hash.Add(M23);
+ hash.Add(M24);
+ hash.Add(M31);
+ hash.Add(M32);
+ hash.Add(M33);
+ hash.Add(M34);
+ hash.Add(OffsetX);
+ hash.Add(OffsetY);
+ hash.Add(OffsetZ);
+ hash.Add(M44);
+ return hash.ToHashCode();
+ }
- public string ToString(IFormatProvider provider) { throw new NotImplementedException(); }
+ ///
+ public override string ToString() => ToString(null, null);
- public string ToString(string format, IFormatProvider provider) { throw new NotImplementedException(); }
+ ///
+ /// Creates a string representation of this Matrix3D.
+ ///
+ public string ToString(IFormatProvider provider) => ToString(null, provider);
- public static Matrix3D operator *(Matrix3D matrix1, Matrix3D matrix2) { throw new NotImplementedException(); }
+ ///
+ /// Creates a string representation of this Matrix3D.
+ ///
+ public string ToString(string format, IFormatProvider provider)
+ {
+ var separator = ",";
+ return string.Format(provider,
+ "{1}{0}{2}{0}{3}{0}{4}{0}{5}{0}{6}{0}{7}{0}{8}{0}{9}{0}{10}{0}{11}{0}{12}{0}{13}{0}{14}{0}{15}{0}{16}",
+ separator,
+ M11.ToString(format, provider), M12.ToString(format, provider), M13.ToString(format, provider), M14.ToString(format, provider),
+ M21.ToString(format, provider), M22.ToString(format, provider), M23.ToString(format, provider), M24.ToString(format, provider),
+ M31.ToString(format, provider), M32.ToString(format, provider), M33.ToString(format, provider), M34.ToString(format, provider),
+ OffsetX.ToString(format, provider), OffsetY.ToString(format, provider), OffsetZ.ToString(format, provider), M44.ToString(format, provider));
+ }
- public static bool operator ==(Matrix3D matrix1, Matrix3D matrix2) { throw new NotImplementedException(); }
+ ///
+ /// Converts this Matrix3D to a System.Numerics.Matrix4x4.
+ ///
+ internal Matrix4x4 ToMatrix4x4() => new Matrix4x4(
+ (float)M11, (float)M12, (float)M13, (float)M14,
+ (float)M21, (float)M22, (float)M23, (float)M24,
+ (float)M31, (float)M32, (float)M33, (float)M34,
+ (float)OffsetX, (float)OffsetY, (float)OffsetZ, (float)M44);
+
+ ///
+ /// Creates a Matrix3D from a System.Numerics.Matrix4x4.
+ ///
+ internal static Matrix3D FromMatrix4x4(Matrix4x4 matrix) => new Matrix3D(
+ matrix.M11, matrix.M12, matrix.M13, matrix.M14,
+ matrix.M21, matrix.M22, matrix.M23, matrix.M24,
+ matrix.M31, matrix.M32, matrix.M33, matrix.M34,
+ matrix.M41, matrix.M42, matrix.M43, matrix.M44);
+
+ private double GetDeterminant()
+ {
+ // Using cofactor expansion along the first row
+ double a = M11 * GetMinor3x3(M22, M23, M24, M32, M33, M34, OffsetY, OffsetZ, M44);
+ double b = M12 * GetMinor3x3(M21, M23, M24, M31, M33, M34, OffsetX, OffsetZ, M44);
+ double c = M13 * GetMinor3x3(M21, M22, M24, M31, M32, M34, OffsetX, OffsetY, M44);
+ double d = M14 * GetMinor3x3(M21, M22, M23, M31, M32, M33, OffsetX, OffsetY, OffsetZ);
+ return a - b + c - d;
+ }
- public static bool operator !=(Matrix3D matrix1, Matrix3D matrix2) { throw new NotImplementedException(); }
+ private static double GetMinor3x3(
+ double m11, double m12, double m13,
+ double m21, double m22, double m23,
+ double m31, double m32, double m33)
+ {
+ return m11 * (m22 * m33 - m23 * m32)
+ - m12 * (m21 * m33 - m23 * m31)
+ + m13 * (m21 * m32 - m22 * m31);
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/PlaneProjection.cs b/src/Uno.UI/UI/Xaml/Media/PlaneProjection.cs
new file mode 100644
index 000000000000..08fd283e15ef
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/PlaneProjection.cs
@@ -0,0 +1,415 @@
+#if __SKIA__
+using System;
+using System.Numerics;
+using Windows.Foundation;
+using Microsoft.UI.Xaml.Media.Media3D;
+
+namespace Microsoft.UI.Xaml.Media;
+
+///
+/// Represents a perspective transform (a 3-D-like effect) on an object.
+///
+public partial class PlaneProjection : Projection
+{
+ // Perspective depth constant matching WinUI's PlaneProjection.h (|ZOffset| = 999).
+ // This value controls how pronounced the 3D perspective effect is. Larger values
+ // produce a subtler effect. Do not change — it must match the WinUI behavior.
+ private const float PerspectiveDepth = 999.0f;
+
+ ///
+ /// Initializes a new instance of the PlaneProjection class.
+ ///
+ public PlaneProjection()
+ {
+ }
+
+ #region Dependency Properties
+
+ ///
+ /// Gets or sets the x-coordinate of the center of rotation of the object you rotate.
+ ///
+ public double CenterOfRotationX
+ {
+ get => (double)GetValue(CenterOfRotationXProperty);
+ set => SetValue(CenterOfRotationXProperty, value);
+ }
+
+ ///
+ /// Identifies the CenterOfRotationX dependency property.
+ ///
+ public static DependencyProperty CenterOfRotationXProperty { get; } =
+ DependencyProperty.Register(
+ nameof(CenterOfRotationX),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.5, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the y-coordinate of the center of rotation of the object you rotate.
+ ///
+ public double CenterOfRotationY
+ {
+ get => (double)GetValue(CenterOfRotationYProperty);
+ set => SetValue(CenterOfRotationYProperty, value);
+ }
+
+ ///
+ /// Identifies the CenterOfRotationY dependency property.
+ ///
+ public static DependencyProperty CenterOfRotationYProperty { get; } =
+ DependencyProperty.Register(
+ nameof(CenterOfRotationY),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.5, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the z-coordinate of the center of rotation of the object you rotate.
+ ///
+ public double CenterOfRotationZ
+ {
+ get => (double)GetValue(CenterOfRotationZProperty);
+ set => SetValue(CenterOfRotationZProperty, value);
+ }
+
+ ///
+ /// Identifies the CenterOfRotationZ dependency property.
+ ///
+ public static DependencyProperty CenterOfRotationZProperty { get; } =
+ DependencyProperty.Register(
+ nameof(CenterOfRotationZ),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance that the object is rotated along the x-axis of the plane of the object.
+ ///
+ public double RotationX
+ {
+ get => (double)GetValue(RotationXProperty);
+ set => SetValue(RotationXProperty, value);
+ }
+
+ ///
+ /// Identifies the RotationX dependency property.
+ ///
+ public static DependencyProperty RotationXProperty { get; } =
+ DependencyProperty.Register(
+ nameof(RotationX),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the number of degrees to rotate the object around the y-axis of rotation.
+ ///
+ public double RotationY
+ {
+ get => (double)GetValue(RotationYProperty);
+ set => SetValue(RotationYProperty, value);
+ }
+
+ ///
+ /// Identifies the RotationY dependency property.
+ ///
+ public static DependencyProperty RotationYProperty { get; } =
+ DependencyProperty.Register(
+ nameof(RotationY),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the number of degrees to rotate the object around the z-axis of rotation.
+ ///
+ public double RotationZ
+ {
+ get => (double)GetValue(RotationZProperty);
+ set => SetValue(RotationZProperty, value);
+ }
+
+ ///
+ /// Identifies the RotationZ dependency property.
+ ///
+ public static DependencyProperty RotationZProperty { get; } =
+ DependencyProperty.Register(
+ nameof(RotationZ),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the x-axis of the plane of the object.
+ ///
+ public double LocalOffsetX
+ {
+ get => (double)GetValue(LocalOffsetXProperty);
+ set => SetValue(LocalOffsetXProperty, value);
+ }
+
+ ///
+ /// Identifies the LocalOffsetX dependency property.
+ ///
+ public static DependencyProperty LocalOffsetXProperty { get; } =
+ DependencyProperty.Register(
+ nameof(LocalOffsetX),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the y-axis of the plane of the object.
+ ///
+ public double LocalOffsetY
+ {
+ get => (double)GetValue(LocalOffsetYProperty);
+ set => SetValue(LocalOffsetYProperty, value);
+ }
+
+ ///
+ /// Identifies the LocalOffsetY dependency property.
+ ///
+ public static DependencyProperty LocalOffsetYProperty { get; } =
+ DependencyProperty.Register(
+ nameof(LocalOffsetY),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the z-axis of the plane of the object.
+ ///
+ public double LocalOffsetZ
+ {
+ get => (double)GetValue(LocalOffsetZProperty);
+ set => SetValue(LocalOffsetZProperty, value);
+ }
+
+ ///
+ /// Identifies the LocalOffsetZ dependency property.
+ ///
+ public static DependencyProperty LocalOffsetZProperty { get; } =
+ DependencyProperty.Register(
+ nameof(LocalOffsetZ),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the x-axis of the screen.
+ ///
+ public double GlobalOffsetX
+ {
+ get => (double)GetValue(GlobalOffsetXProperty);
+ set => SetValue(GlobalOffsetXProperty, value);
+ }
+
+ ///
+ /// Identifies the GlobalOffsetX dependency property.
+ ///
+ public static DependencyProperty GlobalOffsetXProperty { get; } =
+ DependencyProperty.Register(
+ nameof(GlobalOffsetX),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the y-axis of the screen.
+ ///
+ public double GlobalOffsetY
+ {
+ get => (double)GetValue(GlobalOffsetYProperty);
+ set => SetValue(GlobalOffsetYProperty, value);
+ }
+
+ ///
+ /// Identifies the GlobalOffsetY dependency property.
+ ///
+ public static DependencyProperty GlobalOffsetYProperty { get; } =
+ DependencyProperty.Register(
+ nameof(GlobalOffsetY),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets or sets the distance the object is translated along the z-axis of the screen.
+ ///
+ public double GlobalOffsetZ
+ {
+ get => (double)GetValue(GlobalOffsetZProperty);
+ set => SetValue(GlobalOffsetZProperty, value);
+ }
+
+ ///
+ /// Identifies the GlobalOffsetZ dependency property.
+ ///
+ public static DependencyProperty GlobalOffsetZProperty { get; } =
+ DependencyProperty.Register(
+ nameof(GlobalOffsetZ),
+ typeof(double),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(0.0, OnProjectionPropertyChanged));
+
+ ///
+ /// Gets the projection matrix that represents this PlaneProjection.
+ ///
+ public Matrix3D ProjectionMatrix
+ {
+ get => (Matrix3D)GetValue(ProjectionMatrixProperty);
+ private set => SetValue(ProjectionMatrixProperty, value);
+ }
+
+ ///
+ /// Identifies the ProjectionMatrix dependency property.
+ ///
+ public static DependencyProperty ProjectionMatrixProperty { get; } =
+ DependencyProperty.Register(
+ nameof(ProjectionMatrix),
+ typeof(Matrix3D),
+ typeof(PlaneProjection),
+ new FrameworkPropertyMetadata(Matrix3D.Identity));
+
+ private static void OnProjectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is PlaneProjection projection)
+ {
+ projection.OnPropertyChanged();
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Calculates the projection matrix for the specified element size.
+ ///
+ ///
+ /// Matrix composition order (ported from WinUI PlaneProjection.cpp):
+ /// Point × [LocalOffset] × [ToCenter] × [RotateX] × [RotateY] × [RotateZ] ×
+ /// [FromCenter] × [GlobalOffset] × [Perspective]
+ ///
+ /// System.Numerics uses row-vector convention: transformedPoint = point × matrix
+ /// When composing: point × (A × B) = (point × A) × B, so A is applied first.
+ ///
+ internal override Matrix4x4 GetProjectionMatrix(Size elementSize)
+ {
+ float width = (float)elementSize.Width;
+ float height = (float)elementSize.Height;
+
+ if (width == 0 || height == 0)
+ {
+ return Matrix4x4.Identity;
+ }
+
+ // Check if we have any 3D effect at all - if not, return identity
+ if (Is2DAligned())
+ {
+ return Matrix4x4.Identity;
+ }
+
+ // Calculate the absolute center of rotation from the relative values
+ // CenterOfRotationX/Y are relative (0.0 to 1.0), CenterOfRotationZ is absolute
+ float centerX = (float)(CenterOfRotationX * width);
+ float centerY = (float)(CenterOfRotationY * height);
+ float centerZ = (float)CenterOfRotationZ;
+
+ // Build the transformation matrix step by step
+ // Using post-multiplication: result = result * newTransform
+ // This applies transforms in the order they are added
+
+ var result = Matrix4x4.Identity;
+
+ // 1. Apply local offset (in object space, before rotation)
+ if (LocalOffsetX != 0 || LocalOffsetY != 0 || LocalOffsetZ != 0)
+ {
+ result = result * Matrix4x4.CreateTranslation(
+ (float)LocalOffsetX,
+ (float)LocalOffsetY,
+ (float)LocalOffsetZ);
+ }
+
+ // 2. Translate so that the center of rotation is at the origin
+ result = result * Matrix4x4.CreateTranslation(-centerX, -centerY, -centerZ);
+
+ // 3. Apply rotations
+ // WinUI uses degrees and specific rotation direction conventions
+ if (RotationX != 0)
+ {
+ // Negate to match WinUI direction (positive rotates top away from viewer)
+ float rotX = (float)(-RotationX * Math.PI / 180.0);
+ result = result * Matrix4x4.CreateRotationX(rotX);
+ }
+ if (RotationY != 0)
+ {
+ // WinUI inverts Y rotation for the visual effect
+ float rotY = (float)(-RotationY * Math.PI / 180.0);
+ result = result * Matrix4x4.CreateRotationY(rotY);
+ }
+ if (RotationZ != 0)
+ {
+ // WinUI inverts Z rotation
+ float rotZ = (float)(-RotationZ * Math.PI / 180.0);
+ result = result * Matrix4x4.CreateRotationZ(rotZ);
+ }
+
+ // 4. Translate back from center of rotation
+ result = result * Matrix4x4.CreateTranslation(centerX, centerY, centerZ);
+
+ // 5. Apply global offset (in screen space, after rotation)
+ if (GlobalOffsetX != 0 || GlobalOffsetY != 0 || GlobalOffsetZ != 0)
+ {
+ result = result * Matrix4x4.CreateTranslation(
+ (float)GlobalOffsetX,
+ (float)GlobalOffsetY,
+ (float)GlobalOffsetZ);
+ }
+
+ // 6. Apply perspective projection centered on the element
+ // The perspective is applied at the element's center (width/2, height/2)
+ // Using the WinUI perspective depth constant
+ float halfWidth = width / 2;
+ float halfHeight = height / 2;
+
+ // Create the centered perspective matrix
+ // This is: Translate(-halfW, -halfH) × Perspective × Translate(halfW, halfH)
+ // Combined into a single matrix for efficiency
+ var perspective = Matrix4x4.Identity;
+ perspective.M34 = -1.0f / PerspectiveDepth;
+
+ // Apply perspective centered on element
+ result = result * Matrix4x4.CreateTranslation(-halfWidth, -halfHeight, 0);
+ result = result * perspective;
+ result = result * Matrix4x4.CreateTranslation(halfWidth, halfHeight, 0);
+
+ // Update the public ProjectionMatrix property so consumers can read
+ // the last computed matrix. This side effect is intentional — the property
+ // exposes the result of the most recent projection computation.
+ ProjectionMatrix = Matrix3D.FromMatrix4x4(result);
+
+ return result;
+ }
+
+ ///
+ /// Checks if the projection is effectively 2D aligned (no 3D effect).
+ ///
+ ///
+ /// CenterOfRotation values are intentionally not checked here. When all rotations
+ /// and offsets are zero, the translate-to-center and translate-back steps cancel
+ /// out, so CenterOfRotation has no effect on the final matrix.
+ ///
+ private bool Is2DAligned()
+ {
+ return RotationX == 0 &&
+ RotationY == 0 &&
+ RotationZ == 0 &&
+ LocalOffsetX == 0 &&
+ LocalOffsetY == 0 &&
+ LocalOffsetZ == 0 &&
+ GlobalOffsetX == 0 &&
+ GlobalOffsetY == 0 &&
+ GlobalOffsetZ == 0;
+ }
+}
+#endif
diff --git a/src/Uno.UI/UI/Xaml/Media/Projection.cs b/src/Uno.UI/UI/Xaml/Media/Projection.cs
new file mode 100644
index 000000000000..c7d0fe8885d0
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Projection.cs
@@ -0,0 +1,51 @@
+#if __SKIA__
+using System;
+using System.Numerics;
+using Windows.Foundation;
+
+namespace Microsoft.UI.Xaml.Media;
+
+///
+/// Provides a base class for projections, which describe how to transform an object in 3-D space using perspective transforms.
+///
+public partial class Projection : DependencyObject
+{
+ private WeakReference _owner;
+
+ ///
+ /// Initializes a new instance of the Projection class.
+ ///
+ protected Projection()
+ {
+ }
+
+ ///
+ /// Event raised when any property affecting the projection changes.
+ ///
+ internal event EventHandler Changed;
+
+ ///
+ /// Gets or sets the UIElement that owns this projection.
+ ///
+ internal UIElement Owner
+ {
+ get => _owner?.TryGetTarget(out var target) == true ? target : null;
+ set => _owner = value is not null ? new WeakReference(value) : null;
+ }
+
+ ///
+ /// Calculates the projection matrix for the specified element size.
+ ///
+ /// The size of the element being projected.
+ /// The 4x4 projection matrix.
+ internal virtual Matrix4x4 GetProjectionMatrix(Size elementSize) => Matrix4x4.Identity;
+
+ ///
+ /// Raises the Changed event.
+ ///
+ private protected void OnPropertyChanged()
+ {
+ Changed?.Invoke(this, EventArgs.Empty);
+ }
+}
+#endif
diff --git a/src/Uno.UI/UI/Xaml/Media/Projection.reference.cs b/src/Uno.UI/UI/Xaml/Media/Projection.reference.cs
new file mode 100644
index 000000000000..92bf96fdcca5
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Projection.reference.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.UI.Xaml.Media;
+
+public partial class Projection
+{
+ private protected void OnPropertyChanged()
+ {
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs
index 95adc31acbbc..6eb81f331f50 100644
--- a/src/Uno.UI/UI/Xaml/UIElement.cs
+++ b/src/Uno.UI/UI/Xaml/UIElement.cs
@@ -481,6 +481,74 @@ private void OnRenderTransformOriginChanged(Point _, Point origin)
=> _renderTransform?.UpdateOrigin(origin);
#endregion
+#if __SKIA__
+ #region Projection Dependency Property
+
+ private Media.Projection _projection;
+
+ ///
+ /// Gets or sets the perspective projection (3-D effect) to apply when rendering this element.
+ ///
+ public Media.Projection Projection
+ {
+ get => GetProjectionValue();
+ set => SetProjectionValue(value);
+ }
+
+ ///
+ /// Backing dependency property for
+ ///
+ [GeneratedDependencyProperty(DefaultValue = null, ChangedCallback = true)]
+ public static DependencyProperty ProjectionProperty { get; } = CreateProjectionProperty();
+
+ private void OnProjectionChanged(Media.Projection oldValue, Media.Projection newValue)
+ {
+ if (oldValue is not null)
+ {
+ oldValue.Changed -= OnProjectionPropertyChanged;
+ oldValue.Owner = null;
+ }
+
+ _projection = newValue;
+
+ if (newValue is not null)
+ {
+ newValue.Owner = this;
+ newValue.Changed += OnProjectionPropertyChanged;
+ }
+
+ // Update the visual transform
+ UpdateProjection();
+ }
+
+ private void OnProjectionPropertyChanged(object sender, EventArgs e)
+ {
+ UpdateProjection();
+ }
+
+ private void UpdateProjection()
+ {
+ // Trigger a transform update through the render transform adapter
+ // The adapter will combine RenderTransform with Projection
+ if (_renderTransform is not null)
+ {
+ _renderTransform.UpdateSize(_renderTransform.CurrentSize);
+ }
+ else if (_projection is not null)
+ {
+ // Create a minimal adapter to apply the projection
+ _renderTransform = new Uno.UI.Media.NativeRenderTransformAdapter(this, RenderTransform, RenderTransformOrigin);
+ }
+ }
+
+ ///
+ /// Gets the current projection for internal use.
+ ///
+ internal Media.Projection GetProjection() => _projection;
+
+ #endregion
+#endif
+
///
/// Attempts to set the focus on the UIElement.
///
diff --git a/src/Uno.UI/UI/Xaml/UIElement.skia.cs b/src/Uno.UI/UI/Xaml/UIElement.skia.cs
index 5a4545cc8f48..d14a61036c42 100644
--- a/src/Uno.UI/UI/Xaml/UIElement.skia.cs
+++ b/src/Uno.UI/UI/Xaml/UIElement.skia.cs
@@ -332,12 +332,26 @@ internal virtual void OnArrangeVisual(Rect rect, Rect? clip)
var visual = Visual;
visual.ArrangeOffset = new Vector3((float)rect.X, (float)rect.Y, 0) + _translation;
visual.Size = new Vector2((float)rect.Width, (float)rect.Height);
- if (_renderTransform is null && !GetFlowDirectionTransform().IsIdentity)
+
+ var hasProjection = _projection is not null;
+ if (_renderTransform is null && (!GetFlowDirectionTransform().IsIdentity || hasProjection))
{
_renderTransform = new NativeRenderTransformAdapter(this, RenderTransform, RenderTransformOrigin);
}
- _renderTransform?.UpdateFlowDirectionTransform();
+ if (_renderTransform is not null)
+ {
+ // Update with the new layout size - this is important for Projection calculations
+ var newSize = new Size(rect.Width, rect.Height);
+ if (_renderTransform.CurrentSize != newSize)
+ {
+ _renderTransform.UpdateSize(newSize);
+ }
+ else
+ {
+ _renderTransform.UpdateFlowDirectionTransform();
+ }
+ }
// The clipping applied by our parent due to layout constraints are pushed to the visual through the LayoutClip property
// This allows special handling of this clipping by the compositor (cf. ContainerVisual.Render).