Browse Source

[Feature] Add FillRule property for Polyline/Polygon (#20159)

* Implement FillRuleProperty on Polyline

* Implement FillRule Property on Polygon

* Add tests

* Added comments to new public APIs

* Added more comments

* More tests

* More tests

* Updated API

* Added render tests

* More render tests

* Reintroducing the original PolylineGeometry constructor

* Updated API file

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
release/11.3.10
Javier Suárez 2 months ago
committed by Julien Lebosquain
parent
commit
2aeed6ff9f
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 25
      src/Avalonia.Base/Media/PolylineGeometry.cs
  2. 16
      src/Avalonia.Controls/Shapes/Polygon.cs
  3. 18
      src/Avalonia.Controls/Shapes/Polyline.cs
  4. 48
      tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs
  5. 63
      tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs
  6. 33
      tests/Avalonia.RenderTests/Shapes/PolygonTests.cs
  7. 69
      tests/Avalonia.RenderTests/Shapes/PolylineTests.cs
  8. BIN
      tests/TestFiles/Skia/Shapes/Polygon/Polygon_FillRule_EvenOdd.expected.png
  9. BIN
      tests/TestFiles/Skia/Shapes/Polygon/Polygon_FillRule_NonZero.expected.png
  10. BIN
      tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_EvenOdd.expected.png
  11. BIN
      tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_NoFill.expected.png
  12. BIN
      tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_NonZero.expected.png

25
src/Avalonia.Base/Media/PolylineGeometry.cs

@ -27,6 +27,7 @@ namespace Avalonia.Media
private IList<Point> _points;
private IDisposable? _pointsObserver;
private readonly FillRule _fillRule;
static PolylineGeometry()
{
@ -40,8 +41,10 @@ namespace Avalonia.Media
public PolylineGeometry()
{
_points = new Points();
_fillRule = FillRule.EvenOdd;
}
/// <summary>
/// <summary>
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
/// </summary>
@ -49,6 +52,17 @@ namespace Avalonia.Media
{
_points = new Points(points);
IsFilled = isFilled;
_fillRule = FillRule.EvenOdd;
}
/// <summary>
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
/// </summary>
public PolylineGeometry(IEnumerable<Point> points, bool isFilled, FillRule fillRule)
{
_points = new Points(points);
IsFilled = isFilled;
_fillRule = fillRule;
}
/// <summary>
@ -70,10 +84,18 @@ namespace Avalonia.Media
set => SetValue(IsFilledProperty, value);
}
/// <summary>
/// Gets how the intersecting areas of the polyline are combined.
/// </summary>
public FillRule FillRule => _fillRule;
/// <inheritdoc/>
public override Geometry Clone()
{
return new PolylineGeometry(Points, IsFilled);
return new PolylineGeometry(Points, IsFilled, _fillRule)
{
Transform = Transform
};
}
private protected sealed override IGeometryImpl? CreateDefiningGeometry()
@ -83,6 +105,7 @@ namespace Avalonia.Media
using (var context = geometry.Open())
{
context.SetFillRule(_fillRule);
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)

16
src/Avalonia.Controls/Shapes/Polygon.cs

@ -9,9 +9,12 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<IList<Point>> PointsProperty =
AvaloniaProperty.Register<Polygon, IList<Point>>("Points");
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<Polygon, FillRule>(nameof(FillRule));
static Polygon()
{
AffectsGeometry<Polygon>(PointsProperty);
AffectsGeometry<Polygon>(PointsProperty, FillRuleProperty);
}
public Polygon()
@ -25,9 +28,18 @@ namespace Avalonia.Controls.Shapes
set => SetValue(PointsProperty, value);
}
/// <summary>
/// Gets or sets how the interior of the polygon is determined when a <see cref="Shape.Fill"/> is applied.
/// </summary>
public FillRule FillRule
{
get => GetValue(FillRuleProperty);
set => SetValue(FillRuleProperty, value);
}
protected override Geometry CreateDefiningGeometry()
{
return new PolylineGeometry { Points = Points, IsFilled = true };
return new PolylineGeometry(Points, isFilled: true, fillRule: FillRule);
}
}
}

18
src/Avalonia.Controls/Shapes/Polyline.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Media;
using Avalonia.Data;
@ -10,10 +11,13 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<IList<Point>> PointsProperty =
AvaloniaProperty.Register<Polyline, IList<Point>>("Points");
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<Polyline, FillRule>(nameof(FillRule));
static Polyline()
{
StrokeThicknessProperty.OverrideDefaultValue<Polyline>(1);
AffectsGeometry<Polyline>(PointsProperty);
AffectsGeometry<Polyline>(PointsProperty, FillRuleProperty);
}
public Polyline()
@ -27,9 +31,19 @@ namespace Avalonia.Controls.Shapes
set => SetValue(PointsProperty, value);
}
/// <summary>
/// Gets or sets how the interior of the polyline is determined when a <see cref="Shape.Fill"/> is applied.
/// </summary>
public FillRule FillRule
{
get => GetValue(FillRuleProperty);
set => SetValue(FillRuleProperty, value);
}
protected override Geometry CreateDefiningGeometry()
{
return new PolylineGeometry { Points = Points, IsFilled = false };
var isFilled = Fill != null;
return new PolylineGeometry(Points, isFilled, FillRule);
}
}
}

48
tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs

@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -25,4 +26,51 @@ public class PolygonTests
root.Child = null;
}
[Fact]
public void FillRule_On_Polygon_Is_Applied_To_DefiningGeometry()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var target = new Polygon
{
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
FillRule = FillRule.NonZero
};
target.Measure(Size.Infinity);
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
Assert.Equal(FillRule.NonZero, geometry.FillRule);
}
[Fact]
public void Polygon_Equals_Closed_Polyline_Bounds()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var polyline = new Polyline
{
Points = new Points
{
new Point(0, 0),
new Point(10, 0),
new Point(10, 10),
new Point(0, 10),
new Point(0, 0)
},
FillRule = FillRule.NonZero
};
var polygon = new Polygon
{
Points = new Points { new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10) },
FillRule = FillRule.NonZero
};
polyline.Measure(Size.Infinity);
polygon.Measure(Size.Infinity);
Assert.Equal(polygon.DefiningGeometry!.Bounds, polyline.DefiningGeometry!.Bounds);
}
}

63
tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs

@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -25,4 +26,66 @@ public class PolylineTests
root.Child = null;
}
[Fact]
public void FillRule_On_Polyline_Is_Applied_To_DefiningGeometry()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var target = new Polyline
{
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
Fill = Brushes.Red,
FillRule = FillRule.NonZero
};
target.Measure(Size.Infinity);
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
Assert.Equal(FillRule.NonZero, geometry.FillRule);
Assert.True(geometry.IsFilled);
}
[Fact]
public void FillRule_Differs_Between_EvenOdd_And_NonZero()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var evenOdd = new Polyline
{
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
Fill = Brushes.Red,
FillRule = FillRule.EvenOdd
};
var nonZero = new Polyline
{
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
Fill = Brushes.Red,
FillRule = FillRule.NonZero
};
evenOdd.Measure(Size.Infinity);
nonZero.Measure(Size.Infinity);
Assert.Equal(FillRule.EvenOdd, Assert.IsType<PolylineGeometry>(evenOdd.DefiningGeometry).FillRule);
Assert.Equal(FillRule.NonZero, Assert.IsType<PolylineGeometry>(nonZero.DefiningGeometry).FillRule);
}
[Fact]
public void When_Fill_Is_Null_Polyline_Geometry_Is_Not_Filled()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var target = new Polyline
{
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
FillRule = FillRule.NonZero,
Fill = null
};
target.Measure(Size.Infinity);
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
Assert.False(geometry.IsFilled);
}
}

33
tests/Avalonia.RenderTests/Shapes/PolygonTests.cs

@ -17,6 +17,39 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
{
}
[Theory]
[InlineData(FillRule.EvenOdd)]
[InlineData(FillRule.NonZero)]
public async Task Polygon_FillRule(FillRule fillRule)
{
var target = new Decorator
{
Padding = new Thickness(8),
Width = 220,
Height = 220,
Child = new Polygon
{
Stroke = Brushes.Black,
StrokeThickness = 2,
Fill = Brushes.Gold,
Points = new Points
{
new Point(50, 0),
new Point(21, 90),
new Point(98, 35),
new Point(2, 35),
new Point(79, 90)
},
Stretch = Stretch.Uniform,
FillRule = fillRule
}
};
var testName = $"{nameof(Polygon_FillRule)}_{fillRule}";
await RenderToFile(target, testName);
CompareImages(testName);
}
[Fact]
public async Task Polygon_1px_Stroke()
{

69
tests/Avalonia.RenderTests/Shapes/PolylineTests.cs

@ -17,6 +17,75 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
{
}
[Theory]
[InlineData(FillRule.EvenOdd)]
[InlineData(FillRule.NonZero)]
public async Task Polyline_FillRule(FillRule fillRule)
{
var target = new Decorator
{
Padding = new Thickness(8),
Width = 260,
Height = 180,
Child = new Polyline
{
Stroke = Brushes.Black,
StrokeThickness = 2,
Fill = Brushes.OrangeRed,
Points = new Points
{
new Point(10, 170),
new Point(60, 20),
new Point(110, 170),
new Point(20, 70),
new Point(240, 70),
new Point(130, 170),
new Point(190, 20),
new Point(10, 170),
},
Stretch = Stretch.Uniform,
FillRule = fillRule
}
};
var testName = $"{nameof(Polyline_FillRule)}_{fillRule}";
await RenderToFile(target, testName);
CompareImages(testName);
}
[Fact]
public async Task Polyline_FillRule_NoFill()
{
var target = new Decorator
{
Padding = new Thickness(8),
Width = 260,
Height = 180,
Child = new Polyline
{
Stroke = Brushes.Black,
StrokeThickness = 2,
Fill = null,
Points = new Points
{
new Point(10, 170),
new Point(60, 20),
new Point(110, 170),
new Point(20, 70),
new Point(240, 70),
new Point(130, 170),
new Point(190, 20),
new Point(10, 170),
},
Stretch = Stretch.Uniform,
FillRule = FillRule.EvenOdd
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task Polyline_1px_Stroke()
{

BIN
tests/TestFiles/Skia/Shapes/Polygon/Polygon_FillRule_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
tests/TestFiles/Skia/Shapes/Polygon/Polygon_FillRule_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_NoFill.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
tests/TestFiles/Skia/Shapes/Polyline/Polyline_FillRule_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Loading…
Cancel
Save