Skip to content

Commit 8e56298

Browse files
Merge pull request #22620 from unoplatform/dev/mazi/animgeometrytrans
feat: Support `Geometry.Transform`
2 parents d87cc8b + 16bf3c3 commit 8e56298

File tree

9 files changed

+415
-4
lines changed

9 files changed

+415
-4
lines changed

src/SamplesApp/UITests.Shared/UITests.Shared.projitems

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5536,6 +5536,10 @@
55365536
<SubType>Designer</SubType>
55375537
<Generator>MSBuild:Compile</Generator>
55385538
</Page>
5539+
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\PathTestsControl\Path_GeometryTransform.xaml">
5540+
<SubType>Designer</SubType>
5541+
<Generator>MSBuild:Compile</Generator>
5542+
</Page>
55395543
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\PathTestsControl\PathGeometry_Showcase.xaml">
55405544
<SubType>Designer</SubType>
55415545
<Generator>MSBuild:Compile</Generator>
@@ -9177,6 +9181,9 @@
91779181
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\PathTestsControl\Path_Geometry.xaml.cs">
91789182
<DependentUpon>Path_Geometry.xaml</DependentUpon>
91799183
</Compile>
9184+
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\PathTestsControl\Path_GeometryTransform.xaml.cs">
9185+
<DependentUpon>Path_GeometryTransform.xaml</DependentUpon>
9186+
</Compile>
91809187
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\PathTestsControl\PathGeometry_Showcase.xaml.cs">
91819188
<DependentUpon>PathGeometry_Showcase.xaml</DependentUpon>
91829189
</Compile>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<UserControl x:Class="UITests.Windows_UI_Xaml_Shapes.PathTestsControl.Path_GeometryTransform"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
mc:Ignorable="d"
7+
d:DesignHeight="600"
8+
d:DesignWidth="400">
9+
10+
<!-- Manual test for Geometry.Transform (issue #3238)
11+
Expected result: each Path row shows the geometry shape transformed as described in the label.
12+
The transform is applied to the Geometry itself, not to the Path element.
13+
A yellow background makes it easy to see the shape's original bounds vs the transformed shape. -->
14+
15+
<ScrollViewer>
16+
<StackPanel Spacing="16" Margin="16">
17+
18+
<TextBlock Style="{ThemeResource TitleTextBlockStyle}" Text="Geometry.Transform" />
19+
<TextBlock TextWrapping="Wrap"
20+
Text="Verifies that Transform applied to a Geometry object is rendered correctly. Each row shows the same 40x20 RectangleGeometry with a different transform. The yellow border marks the Path element bounds." />
21+
22+
<!-- Baseline: no transform -->
23+
<StackPanel>
24+
<TextBlock FontWeight="SemiBold" Text="Baseline — no transform" />
25+
<TextBlock Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
26+
Text="Expected: blue rectangle at origin (0,0) of the yellow container." />
27+
<Border Background="Yellow" Width="200" Height="80" HorizontalAlignment="Left">
28+
<Path Fill="Blue">
29+
<Path.Data>
30+
<RectangleGeometry Rect="10,10 40,20" />
31+
</Path.Data>
32+
</Path>
33+
</Border>
34+
</StackPanel>
35+
36+
<!-- TranslateTransform -->
37+
<StackPanel>
38+
<TextBlock FontWeight="SemiBold" Text="TranslateTransform (X=30, Y=20)" />
39+
<TextBlock TextWrapping="Wrap"
40+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
41+
Text="Expected: blue rectangle shifted 30px right and 20px down relative to the baseline." />
42+
<Border Background="Yellow" Width="200" Height="80" HorizontalAlignment="Left">
43+
<Path Fill="Blue">
44+
<Path.Data>
45+
<RectangleGeometry Rect="10,10 40,20">
46+
<RectangleGeometry.Transform>
47+
<TranslateTransform X="30" Y="20" />
48+
</RectangleGeometry.Transform>
49+
</RectangleGeometry>
50+
</Path.Data>
51+
</Path>
52+
</Border>
53+
</StackPanel>
54+
55+
<!-- RotateTransform -->
56+
<StackPanel>
57+
<TextBlock FontWeight="SemiBold" Text="RotateTransform (Angle=45)" />
58+
<TextBlock TextWrapping="Wrap"
59+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
60+
Text="Expected: blue rectangle rotated 45° around the origin of the geometry." />
61+
<Border Background="Yellow" Width="200" Height="120" HorizontalAlignment="Left">
62+
<Path Fill="Blue">
63+
<Path.Data>
64+
<RectangleGeometry Rect="50,50 40,20">
65+
<RectangleGeometry.Transform>
66+
<RotateTransform Angle="45" CenterX="50" CenterY="50" />
67+
</RectangleGeometry.Transform>
68+
</RectangleGeometry>
69+
</Path.Data>
70+
</Path>
71+
</Border>
72+
</StackPanel>
73+
74+
<!-- ScaleTransform -->
75+
<StackPanel>
76+
<TextBlock FontWeight="SemiBold" Text="ScaleTransform (ScaleX=2, ScaleY=2)" />
77+
<TextBlock TextWrapping="Wrap"
78+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
79+
Text="Expected: blue rectangle scaled to 2× in both dimensions (80x40 instead of 40x20)." />
80+
<Border Background="Yellow" Width="200" Height="120" HorizontalAlignment="Left">
81+
<Path Fill="Blue">
82+
<Path.Data>
83+
<RectangleGeometry Rect="10,10 40,20">
84+
<RectangleGeometry.Transform>
85+
<ScaleTransform ScaleX="2" ScaleY="2" />
86+
</RectangleGeometry.Transform>
87+
</RectangleGeometry>
88+
</Path.Data>
89+
</Path>
90+
</Border>
91+
</StackPanel>
92+
93+
<!-- CompositeTransform (translate + rotate) -->
94+
<StackPanel>
95+
<TextBlock FontWeight="SemiBold" Text="CompositeTransform (TranslateX=20, Rotation=30)" />
96+
<TextBlock TextWrapping="Wrap"
97+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
98+
Text="Expected: blue rectangle translated 20px right then rotated 30°." />
99+
<Border Background="Yellow" Width="200" Height="120" HorizontalAlignment="Left">
100+
<Path Fill="Blue">
101+
<Path.Data>
102+
<RectangleGeometry Rect="20,40 40,20">
103+
<RectangleGeometry.Transform>
104+
<CompositeTransform TranslateX="20" Rotation="30" />
105+
</RectangleGeometry.Transform>
106+
</RectangleGeometry>
107+
</Path.Data>
108+
</Path>
109+
</Border>
110+
</StackPanel>
111+
112+
<!-- Dynamic: TranslateTransform driven by sliders -->
113+
<StackPanel>
114+
<TextBlock FontWeight="SemiBold" Text="Dynamic TranslateTransform (use sliders)" />
115+
<TextBlock TextWrapping="Wrap"
116+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
117+
Text="Expected: blue rectangle moves as sliders change, validating that geometry transform changes cause the Path to re-render." />
118+
<Border Background="Yellow" Width="200" Height="120" HorizontalAlignment="Left">
119+
<Path Fill="Blue">
120+
<Path.Data>
121+
<RectangleGeometry Rect="10,10 40,20">
122+
<RectangleGeometry.Transform>
123+
<TranslateTransform X="{Binding Value, ElementName=SliderX}"
124+
Y="{Binding Value, ElementName=SliderY}" />
125+
</RectangleGeometry.Transform>
126+
</RectangleGeometry>
127+
</Path.Data>
128+
</Path>
129+
</Border>
130+
<Slider x:Name="SliderX" Header="TranslateX" Minimum="0" Maximum="120" Value="0" />
131+
<Slider x:Name="SliderY" Header="TranslateY" Minimum="0" Maximum="80" Value="0" />
132+
</StackPanel>
133+
134+
<!-- GeometryGroup with Transform on child geometry -->
135+
<StackPanel>
136+
<TextBlock FontWeight="SemiBold" Text="GeometryGroup with child TranslateTransform" />
137+
<TextBlock TextWrapping="Wrap"
138+
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
139+
Text="Expected: two rectangles — the second (green) shifted 60px right from the first (blue)." />
140+
<Border Background="Yellow" Width="200" Height="80" HorizontalAlignment="Left">
141+
<Path>
142+
<Path.Data>
143+
<GeometryGroup>
144+
<RectangleGeometry Rect="10,10 40,20" />
145+
<RectangleGeometry Rect="10,10 40,20">
146+
<RectangleGeometry.Transform>
147+
<TranslateTransform X="60" />
148+
</RectangleGeometry.Transform>
149+
</RectangleGeometry>
150+
</GeometryGroup>
151+
</Path.Data>
152+
<Path.Fill>
153+
<LinearGradientBrush>
154+
<GradientStop Color="Blue" Offset="0" />
155+
<GradientStop Color="Green" Offset="1" />
156+
</LinearGradientBrush>
157+
</Path.Fill>
158+
</Path>
159+
</Border>
160+
</StackPanel>
161+
162+
</StackPanel>
163+
</ScrollViewer>
164+
</UserControl>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Uno.UI.Samples.Controls;
2+
using Microsoft.UI.Xaml.Controls;
3+
4+
namespace UITests.Windows_UI_Xaml_Shapes.PathTestsControl
5+
{
6+
[Sample("Path", Name = "Path_GeometryTransform", Description = "Verifies Geometry.Transform rendering — each row applies a different transform directly to the Geometry (not the Path element). Skia-specific feature (#3238).")]
7+
public sealed partial class Path_GeometryTransform : UserControl
8+
{
9+
public Path_GeometryTransform()
10+
{
11+
this.InitializeComponent();
12+
}
13+
}
14+
}

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_Geometry.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,64 @@ public void StreamGeometry_GetSKPath_CheckFillType()
153153
var skPath = streamGeometry.GetSKPath();
154154
skPath.FillType.Should().Be(SKPathFillType.EvenOdd);
155155
}
156+
157+
[TestMethod]
158+
public void RectangleGeometry_Transform_Applies_To_SKPath()
159+
{
160+
var geometry = new RectangleGeometry
161+
{
162+
Rect = new Rect(0, 0, 100, 50),
163+
Transform = new TranslateTransform { X = 30, Y = 20 }
164+
};
165+
166+
var untransformed = geometry.GetSKPath();
167+
var transformed = geometry.GetTransformedSKPath();
168+
169+
// Untransformed path should have bounds at origin
170+
Assert.AreEqual(0, untransformed.Bounds.Left, 0.1f);
171+
Assert.AreEqual(0, untransformed.Bounds.Top, 0.1f);
172+
173+
// Transformed path should be offset by the translation
174+
Assert.AreEqual(30, transformed.Bounds.Left, 0.1f);
175+
Assert.AreEqual(20, transformed.Bounds.Top, 0.1f);
176+
Assert.AreEqual(130, transformed.Bounds.Right, 0.1f);
177+
Assert.AreEqual(70, transformed.Bounds.Bottom, 0.1f);
178+
}
179+
180+
[TestMethod]
181+
public void RectangleGeometry_NoTransform_Returns_Same_Path()
182+
{
183+
var geometry = new RectangleGeometry
184+
{
185+
Rect = new Rect(10, 20, 100, 50)
186+
};
187+
188+
var untransformed = geometry.GetSKPath();
189+
var transformed = geometry.GetTransformedSKPath();
190+
191+
// Without a transform, both should have the same bounds
192+
Assert.AreEqual(untransformed.Bounds.Left, transformed.Bounds.Left, 0.1f);
193+
Assert.AreEqual(untransformed.Bounds.Top, transformed.Bounds.Top, 0.1f);
194+
Assert.AreEqual(untransformed.Bounds.Right, transformed.Bounds.Right, 0.1f);
195+
Assert.AreEqual(untransformed.Bounds.Bottom, transformed.Bounds.Bottom, 0.1f);
196+
}
197+
198+
[TestMethod]
199+
public void RectangleGeometry_Transform_Updates_GeometrySource2D()
200+
{
201+
var geometry = new RectangleGeometry
202+
{
203+
Rect = new Rect(0, 0, 100, 50),
204+
Transform = new TranslateTransform { X = 50, Y = 0 }
205+
};
206+
207+
var source = geometry.GetGeometrySource2D();
208+
var path = source.Geometry;
209+
210+
// The path from GetGeometrySource2D should include the transform
211+
Assert.AreEqual(50, path.Bounds.Left, 0.1f);
212+
Assert.AreEqual(150, path.Bounds.Right, 0.1f);
213+
}
156214
#endif
157215
}
158216
}

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Shapes/Given_Path.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,118 @@ public async Task When_PathGeometry_Figures_Not_Filled_ImageBrush()
169169
ImageAssert.HasColorAt(screenShot, new Point(90, 50), "#FF38FF52");
170170
}
171171

172+
[TestMethod]
173+
[GitHubWorkItem("https://github.com/unoplatform/uno/issues/3238")]
174+
#if !__SKIA__ && !WINAPPSDK
175+
[Ignore("Geometry.Transform is only implemented on Skia and WinUI.")]
176+
#endif
177+
public async Task When_Geometry_Transform_Translates_Rendering()
178+
{
179+
var path = new Path
180+
{
181+
Data = new RectangleGeometry
182+
{
183+
Rect = new Rect(0, 0, 50, 50),
184+
Transform = new TranslateTransform { X = 60, Y = 60 }
185+
},
186+
Fill = new SolidColorBrush(Microsoft.UI.Colors.Red),
187+
Width = 150,
188+
Height = 150
189+
};
190+
191+
await UITestHelper.Load(path);
192+
193+
var screenshot = await UITestHelper.ScreenShot(path);
194+
195+
// The rectangle is at (0,0)-(50,50) but translated by (60,60),
196+
// so it should render at (60,60)-(110,110).
197+
// Origin area should NOT be red
198+
ImageAssert.DoesNotHaveColorAt(screenshot, new Point(10, 10), Microsoft.UI.Colors.Red);
199+
// Translated position should be red
200+
ImageAssert.HasColorAt(screenshot, new Point(85, 85), Microsoft.UI.Colors.Red);
201+
}
202+
203+
[TestMethod]
204+
[GitHubWorkItem("https://github.com/unoplatform/uno/issues/3238")]
205+
#if !__SKIA__ && !WINAPPSDK
206+
[Ignore("Geometry.Transform is only implemented on Skia and WinUI.")]
207+
#endif
208+
public async Task When_Geometry_Transform_Changed_At_Runtime()
209+
{
210+
var translate = new TranslateTransform { X = 0, Y = 0 };
211+
var path = new Path
212+
{
213+
Data = new RectangleGeometry
214+
{
215+
Rect = new Rect(0, 0, 50, 50),
216+
Transform = translate
217+
},
218+
Fill = new SolidColorBrush(Microsoft.UI.Colors.Red),
219+
Width = 150,
220+
Height = 150
221+
};
222+
223+
await UITestHelper.Load(path);
224+
225+
// Initially, the rect is at (0,0)
226+
var screenshotBefore = await UITestHelper.ScreenShot(path);
227+
ImageAssert.HasColorAt(screenshotBefore, new Point(25, 25), Microsoft.UI.Colors.Red);
228+
ImageAssert.DoesNotHaveColorAt(screenshotBefore, new Point(125, 125), Microsoft.UI.Colors.Red);
229+
230+
// Change the transform at runtime (simulates what an animation would do)
231+
translate.X = 80;
232+
translate.Y = 80;
233+
234+
await WindowHelper.WaitForIdle();
235+
236+
// After transform change, the rect should have moved
237+
var screenshotAfter = await UITestHelper.ScreenShot(path);
238+
ImageAssert.HasColorAt(screenshotAfter, new Point(105, 105), Microsoft.UI.Colors.Red);
239+
// Origin should no longer be red
240+
ImageAssert.DoesNotHaveColorAt(screenshotAfter, new Point(10, 10), Microsoft.UI.Colors.Red);
241+
}
242+
243+
[TestMethod]
244+
[GitHubWorkItem("https://github.com/unoplatform/uno/issues/3238")]
245+
#if !__SKIA__ && !WINAPPSDK
246+
[Ignore("Geometry.Transform is only implemented on Skia and WinUI.")]
247+
#endif
248+
public async Task When_GeometryGroup_Children_Have_Transforms()
249+
{
250+
var path = new Path
251+
{
252+
Data = new GeometryGroup
253+
{
254+
Children =
255+
{
256+
new RectangleGeometry
257+
{
258+
Rect = new Rect(0, 0, 40, 40),
259+
},
260+
new RectangleGeometry
261+
{
262+
Rect = new Rect(0, 0, 40, 40),
263+
Transform = new TranslateTransform { X = 80, Y = 80 }
264+
}
265+
}
266+
},
267+
Fill = new SolidColorBrush(Microsoft.UI.Colors.Red),
268+
Width = 150,
269+
Height = 150
270+
};
271+
272+
await UITestHelper.Load(path);
273+
274+
var screenshot = await UITestHelper.ScreenShot(path);
275+
276+
// First rect at origin should be red
277+
ImageAssert.HasColorAt(screenshot, new Point(20, 20), Microsoft.UI.Colors.Red);
278+
// Second rect translated to (80,80) should be red
279+
ImageAssert.HasColorAt(screenshot, new Point(100, 100), Microsoft.UI.Colors.Red);
280+
// Gap between the two rects should NOT be red
281+
ImageAssert.DoesNotHaveColorAt(screenshot, new Point(60, 60), Microsoft.UI.Colors.Red);
282+
}
283+
172284
private void Brush_ImageOpened(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => throw new NotImplementedException();
173285
}
174286
}

0 commit comments

Comments
 (0)